gating-metrics 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liu Yuanshui and the ARDS gating-circuit study authors
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 deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: gating-metrics
3
+ Version: 0.1.0
4
+ Summary: Measurement toolkit for state-gated co-expression coupling: layer couplings, exact covariance partition, platform contrast, closed-form power planning
5
+ Author: Liu Yuanshui
6
+ License: MIT
7
+ Project-URL: OSF registration, https://doi.org/10.17605/OSF.IO/4TZH9
8
+ Project-URL: OSF project, https://osf.io/v8w5a
9
+ Project-URL: Homepage, https://github.com/Shuiruolys-Liu/gating-metrics
10
+ Keywords: co-expression,single-cell,gating,ARDS,inflammasome,measurement
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: pandas>=2.0
20
+ Requires-Dist: scipy>=1.10
21
+ Dynamic: license-file
22
+
23
+ # gating_metrics — measurement toolkit for state-gated co-expression coupling
24
+
25
+ Open-source Python toolkit accompanying the ARDS gating-circuit study
26
+ (manifest modules F1/F2/F3, registered 2026-08-18 before execution). It
27
+ formalises the study's core measurement claim — that a state-gated
28
+ hub–module coupling (e.g. KAT2A vs the inflammasome module) is an
29
+ **individual-level, platform-dependent emergent measurement** — into
30
+ reusable, tested routines.
31
+
32
+ ## What it provides
33
+
34
+ | Module | Contents |
35
+ |---|---|
36
+ | `coupling.py` | Layer-wise Spearman coupling: `L_cell` (within-sample per-cell, with detected-gene-count partial sensitivity), `L_indiv` (between-individual pseudobulk), `L_bulk` (bulk-assay). |
37
+ | `partition.py` | Exact method-of-moments (nested ANOVA cross-product) covariance partition: between-individual / between-compartment-within-individual / within components, with sample-level bootstrap CIs. |
38
+ | `platform_contrast.py` (renamed from `platform.py` to avoid the stdlib collision) | Fisher-Z same-compartment cross-platform contrast (the "is it a platform artefact?" test) and the measurement-conditions compatibility report / atlas verdict (frozen classification thresholds: DOMINANT-NEGATIVE r≤−0.2 & p<0.05; DOMINANT-POSITIVE r≥+0.2 & p<0.05; else INDETERMINATE; atlas support = ≥80% DOMINANT-NEGATIVE within a condition class). |
39
+ | `power.py` | Closed-form Fisher-Z planning tools: n for a target coupling, power at n, minimum detectable |r|. |
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import pandas as pd
45
+ from coupling import layer_cell, layer_indiv
46
+ from partition import partition, bootstrap_share_between
47
+
48
+ # cells: one row per cell with columns sample, compartment, x (hub gene),
49
+ # y (module score), ng (detected-gene count)
50
+ per_group, summary = layer_cell(cells, ["sample", "compartment"])
51
+ r_indiv, p_indiv = layer_indiv(pseudobulk) # one row per sample
52
+ part = partition(cells, by=("sample", "compartment"))
53
+ lo, hi = bootstrap_share_between(cells, by=("sample", "compartment"))
54
+ ```
55
+
56
+ Self-tests (synthetic two-level data, seed 42; partition identity, power
57
+ closed form, Fisher-Z sign reversal, classification thresholds):
58
+
59
+ ```
60
+ python test_gating_metrics.py
61
+ ```
62
+
63
+ ## Registered falsifiable prediction (public test for the field)
64
+
65
+ Any new **acute-inflammation whole-blood cohort assayed in bulk** (array or
66
+ bulk RNA-seq) should reproduce a **negative reference-stratum hub–module
67
+ coupling**; **scRNA pseudobulk and protein layers are expected non-negative**
68
+ (same compartment). The F1/F2 tables in the companion manuscript are the
69
+ current evidence base; the prediction is falsifiable by any cohort that
70
+ violates the sign-by-condition pattern.
71
+
72
+ **Public registration**: OSF Preregistration, 2026-08-18 —
73
+ DOI [10.17605/OSF.IO/4TZH9](https://doi.org/10.17605/OSF.IO/4TZH9)
74
+ (associated project https://osf.io/v8w5a).
75
+
76
+ **External validation status (recorded 2026-08-19, honest)**: the authors'
77
+ own value-blind external hold-out (module G1; four never-used
78
+ acute-inflammation blood cohorts GSE66099/GSE40012/GSE57065/GSE37069)
79
+ did **NOT** confirm the reference-stratum prediction (2/4 direction-negative;
80
+ pooled fixed-effect r=+0.004). The registered text above is kept unchanged as
81
+ the auditable record; by the external data, the reference-stratum claim is
82
+ refuted and the inflamed-stratum negative coupling is supported (manuscript
83
+ TableS36). Independent replication by other groups remains the intended
84
+ falsification channel.
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install gating-metrics # once published on PyPI (pending author action)
90
+ # or from source:
91
+ git clone https://github.com/Shuiruolys-Liu/gating-metrics
92
+ cd gating-metrics && pip install .
93
+ ```
94
+
95
+ ## Provenance
96
+
97
+ Metric definitions are copied verbatim from the frozen F1 decomposition
98
+ dictionary (`manifest.json -> preregistrations.modules.
99
+ F1_variance_decomposition_gating`, registered 2026-08-18). No metric in this
100
+ package was tuned after inspecting the outcome data.
101
+
102
+ ## Citing
103
+
104
+ If you use this toolkit, please cite the companion manuscript (under
105
+ preparation) and the OSF registration (DOI 10.17605/OSF.IO/4TZH9).
@@ -0,0 +1,83 @@
1
+ # gating_metrics — measurement toolkit for state-gated co-expression coupling
2
+
3
+ Open-source Python toolkit accompanying the ARDS gating-circuit study
4
+ (manifest modules F1/F2/F3, registered 2026-08-18 before execution). It
5
+ formalises the study's core measurement claim — that a state-gated
6
+ hub–module coupling (e.g. KAT2A vs the inflammasome module) is an
7
+ **individual-level, platform-dependent emergent measurement** — into
8
+ reusable, tested routines.
9
+
10
+ ## What it provides
11
+
12
+ | Module | Contents |
13
+ |---|---|
14
+ | `coupling.py` | Layer-wise Spearman coupling: `L_cell` (within-sample per-cell, with detected-gene-count partial sensitivity), `L_indiv` (between-individual pseudobulk), `L_bulk` (bulk-assay). |
15
+ | `partition.py` | Exact method-of-moments (nested ANOVA cross-product) covariance partition: between-individual / between-compartment-within-individual / within components, with sample-level bootstrap CIs. |
16
+ | `platform_contrast.py` (renamed from `platform.py` to avoid the stdlib collision) | Fisher-Z same-compartment cross-platform contrast (the "is it a platform artefact?" test) and the measurement-conditions compatibility report / atlas verdict (frozen classification thresholds: DOMINANT-NEGATIVE r≤−0.2 & p<0.05; DOMINANT-POSITIVE r≥+0.2 & p<0.05; else INDETERMINATE; atlas support = ≥80% DOMINANT-NEGATIVE within a condition class). |
17
+ | `power.py` | Closed-form Fisher-Z planning tools: n for a target coupling, power at n, minimum detectable |r|. |
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ import pandas as pd
23
+ from coupling import layer_cell, layer_indiv
24
+ from partition import partition, bootstrap_share_between
25
+
26
+ # cells: one row per cell with columns sample, compartment, x (hub gene),
27
+ # y (module score), ng (detected-gene count)
28
+ per_group, summary = layer_cell(cells, ["sample", "compartment"])
29
+ r_indiv, p_indiv = layer_indiv(pseudobulk) # one row per sample
30
+ part = partition(cells, by=("sample", "compartment"))
31
+ lo, hi = bootstrap_share_between(cells, by=("sample", "compartment"))
32
+ ```
33
+
34
+ Self-tests (synthetic two-level data, seed 42; partition identity, power
35
+ closed form, Fisher-Z sign reversal, classification thresholds):
36
+
37
+ ```
38
+ python test_gating_metrics.py
39
+ ```
40
+
41
+ ## Registered falsifiable prediction (public test for the field)
42
+
43
+ Any new **acute-inflammation whole-blood cohort assayed in bulk** (array or
44
+ bulk RNA-seq) should reproduce a **negative reference-stratum hub–module
45
+ coupling**; **scRNA pseudobulk and protein layers are expected non-negative**
46
+ (same compartment). The F1/F2 tables in the companion manuscript are the
47
+ current evidence base; the prediction is falsifiable by any cohort that
48
+ violates the sign-by-condition pattern.
49
+
50
+ **Public registration**: OSF Preregistration, 2026-08-18 —
51
+ DOI [10.17605/OSF.IO/4TZH9](https://doi.org/10.17605/OSF.IO/4TZH9)
52
+ (associated project https://osf.io/v8w5a).
53
+
54
+ **External validation status (recorded 2026-08-19, honest)**: the authors'
55
+ own value-blind external hold-out (module G1; four never-used
56
+ acute-inflammation blood cohorts GSE66099/GSE40012/GSE57065/GSE37069)
57
+ did **NOT** confirm the reference-stratum prediction (2/4 direction-negative;
58
+ pooled fixed-effect r=+0.004). The registered text above is kept unchanged as
59
+ the auditable record; by the external data, the reference-stratum claim is
60
+ refuted and the inflamed-stratum negative coupling is supported (manuscript
61
+ TableS36). Independent replication by other groups remains the intended
62
+ falsification channel.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install gating-metrics # once published on PyPI (pending author action)
68
+ # or from source:
69
+ git clone https://github.com/Shuiruolys-Liu/gating-metrics
70
+ cd gating-metrics && pip install .
71
+ ```
72
+
73
+ ## Provenance
74
+
75
+ Metric definitions are copied verbatim from the frozen F1 decomposition
76
+ dictionary (`manifest.json -> preregistrations.modules.
77
+ F1_variance_decomposition_gating`, registered 2026-08-18). No metric in this
78
+ package was tuned after inspecting the outcome data.
79
+
80
+ ## Citing
81
+
82
+ If you use this toolkit, please cite the companion manuscript (under
83
+ preparation) and the OSF registration (DOI 10.17605/OSF.IO/4TZH9).
@@ -0,0 +1 @@
1
+ """gating_metrics — see README.md; modules: coupling, partition, platform_contrast, power."""
@@ -0,0 +1,84 @@
1
+ """gating_metrics.coupling — layer-wise coupling metrics for state-gated
2
+ co-expression structures (channel-B methodology of the ARDS gating study).
3
+
4
+ Definitions are copied verbatim from the frozen F1 decomposition dictionary
5
+ (manifest F1_variance_decomposition_gating, registered 2026-08-18):
6
+ coupling metric = Spearman correlation between the hub gene (e.g. KAT2A)
7
+ and the module (e.g. mean of the inflammasome genes), evaluated at three
8
+ layers:
9
+ L_cell — within-sample, within-compartment, per-cell correlation, plus a
10
+ detected-gene-count partial sensitivity (library-complexity
11
+ background);
12
+ L_indiv — between-individual pseudobulk correlation across samples;
13
+ L_bulk — bulk-assay correlation across samples.
14
+ """
15
+ import numpy as np
16
+ from scipy import stats
17
+
18
+
19
+ def spearman_np(a, b, min_n=4):
20
+ """Spearman r and p over the jointly-finite values of a and b."""
21
+ a, b = np.asarray(a, float), np.asarray(b, float)
22
+ m = np.isfinite(a) & np.isfinite(b)
23
+ if m.sum() < min_n or np.std(a[m]) == 0 or np.std(b[m]) == 0:
24
+ return np.nan, np.nan
25
+ return stats.spearmanr(a[m], b[m])
26
+
27
+
28
+ def partial_spearman(a, b, c, min_n=8):
29
+ """Rank-partial Spearman of a vs b controlling c (residualised ranks)."""
30
+ a, b, c = np.asarray(a, float), np.asarray(b, float), np.asarray(c, float)
31
+ m = np.isfinite(a) & np.isfinite(b) & np.isfinite(c)
32
+ if m.sum() < min_n:
33
+ return np.nan, np.nan
34
+ ra, rb, rc = (stats.rankdata(v[m]) for v in (a, b, c))
35
+
36
+ def resid(y):
37
+ X = np.column_stack([np.ones_like(rc), rc])
38
+ beta, *_ = np.linalg.lstsq(X, y, rcond=None)
39
+ return y - X @ beta
40
+
41
+ ea, eb = resid(ra), resid(rb)
42
+ if np.std(ea) == 0 or np.std(eb) == 0:
43
+ return np.nan, np.nan
44
+ return stats.pearsonr(ea, eb)
45
+
46
+
47
+ def layer_cell(df, group_cols, x="x", y="y", background="ng", min_cells=30):
48
+ """L_cell: per-group within-sample Spearman (x vs y) and its partial
49
+ (controlling `background`, e.g. per-cell detected-gene count).
50
+
51
+ df: long dataframe with one row per CELL plus the group columns
52
+ (e.g. ['sample','compartment']); returns per-group r/p plus the
53
+ median-across-groups summary.
54
+ """
55
+ rows = []
56
+ for key, g in df.groupby(group_cols):
57
+ if len(g) < min_cells:
58
+ continue
59
+ r, p = spearman_np(g[x].values, g[y].values)
60
+ rp, pp = partial_spearman(g[x].values, g[y].values, g[background].values) \
61
+ if background in g else (np.nan, np.nan)
62
+ rows.append(dict(zip(group_cols, key if isinstance(key, tuple) else (key,)),
63
+ n_cells=len(g), r=r, p=p, r_partial=r, p_partial=pp))
64
+ per = pd.DataFrame(rows) if rows else None
65
+ import pandas as pd
66
+ per = pd.DataFrame(rows)
67
+ if len(per):
68
+ summary = dict(median_r=float(np.nanmedian(per.r)),
69
+ median_r_partial=float(np.nanmedian(per.r_partial)),
70
+ n_groups=int(len(per)))
71
+ else:
72
+ summary = dict(median_r=np.nan, median_r_partial=np.nan, n_groups=0)
73
+ return per, summary
74
+
75
+
76
+ def layer_indiv(pb, x="x", y="y", stratum=None, min_n=8):
77
+ """L_indiv: between-individual Spearman across sample-level pseudobulk
78
+ values (pb: one row per sample[ x stratum])."""
79
+ if stratum:
80
+ out = {}
81
+ for s, g in pb.dropna(subset=[x, y]).groupby(stratum):
82
+ out[s] = spearman_np(g[x].values, g[y].values) if len(g) >= min_n else (np.nan, np.nan)
83
+ return out
84
+ return spearman_np(pb[x].values, pb[y].values)
@@ -0,0 +1,85 @@
1
+ """gating_metrics.partition — exact method-of-moments (nested ANOVA
2
+ cross-product) decomposition of a two-variable covariance across hierarchy
3
+ levels (frozen F1 dictionary): between-individual (between-sample),
4
+ between-compartment-within-sample, and within-sample residual components.
5
+
6
+ The partition is exact algebra:
7
+ SSP_tot = sum_ij (x_ij - xbar)(y_ij - ybar)
8
+ SSP_between = sum_s n_s (xbar_s - xbar)(ybar_s - ybar)
9
+ SSP_comp = sum_{s,c} n_sc (xbar_sc - xbar_s)(ybar_sc - ybar_s)
10
+ SSP_within = SSP_tot - SSP_between - SSP_comp
11
+ with bootstrap CIs obtained by resampling INDIVIDUALS (samples), the
12
+ independent units of the hierarchy.
13
+ """
14
+ import numpy as np
15
+ import pandas as pd
16
+
17
+
18
+ def group_stats(df, x="x", y="y", by=("sample", "comp")):
19
+ d = df.copy()
20
+ d["xy"] = d[x] * d[y]
21
+ return d.groupby(list(by)).agg(n=(x, "size"), sx=(x, "sum"), sy=(y, "sum"),
22
+ sxy=("xy", "sum")).reset_index()
23
+
24
+
25
+ def partition_from_gs(gs):
26
+ """Partition from per-group sufficient statistics (n, sx, sy, sxy)."""
27
+ N = gs.n.sum()
28
+ xbar, ybar = gs.sx.sum() / N, gs.sy.sum() / N
29
+ SSP_tot = gs.sxy.sum() - N * xbar * ybar
30
+ gs = gs.copy()
31
+ gs["mx"], gs["my"] = gs.sx / gs.n, gs.sy / gs.n
32
+ ss_rows = []
33
+ for s, g in gs.groupby("sample" if "sample" in gs.columns else gs.columns[0]):
34
+ ss_rows.append({"sample": s, "n": g.n.sum(),
35
+ "mxs": np.average(g.mx, weights=g.n),
36
+ "mys": np.average(g.my, weights=g.n)})
37
+ ss = pd.DataFrame(ss_rows)
38
+ gcol = "sample" if "sample" in gs.columns else gs.columns[0]
39
+ SSP_B = float((ss.n * (ss.mxs - xbar) * (ss.mys - ybar)).sum())
40
+ merged = gs.merge(ss[[gcol, "mxs", "mys"]], on=gcol)
41
+ if len(gs.columns) > 4: # compartment level present
42
+ SSP_C = float((merged.n * (merged.mx - merged.mxs) *
43
+ (merged.my - merged.mys)).sum())
44
+ else:
45
+ SSP_C = 0.0
46
+ SSP_W = SSP_tot - SSP_B - SSP_C
47
+ tot = SSP_tot if SSP_tot != 0 else np.nan
48
+ return dict(n_cells=int(N), n_groups=len(gs), SSP_tot=SSP_tot,
49
+ SSP_between_individual=SSP_B,
50
+ SSP_between_compartment_within=SSP_C, SSP_within=SSP_W,
51
+ share_between_individual=SSP_B / tot if tot == tot else np.nan,
52
+ share_between_compartment=SSP_C / tot if tot == tot else np.nan,
53
+ share_within=SSP_W / tot if tot == tot else np.nan)
54
+
55
+
56
+ def partition(df, x="x", y="y", by=("sample", "comp")):
57
+ """Convenience wrapper: partition + algebraic identity assert."""
58
+ gs = group_stats(df, x, y, by)
59
+ out = partition_from_gs(gs)
60
+ if by == ("sample",):
61
+ assert abs(out["SSP_tot"] - out["SSP_between_individual"]
62
+ - out["SSP_within"]) < 1e-6 * max(1, abs(out["SSP_tot"]))
63
+ else:
64
+ assert abs(out["SSP_tot"] - out["SSP_between_individual"]
65
+ - out["SSP_between_compartment_within"]
66
+ - out["SSP_within"]) < 1e-6 * max(1, abs(out["SSP_tot"]))
67
+ return out
68
+
69
+
70
+ def bootstrap_share_between(df, x="x", y="y", by=("sample", "comp"),
71
+ n_boot=1000, seed=42):
72
+ """Bootstrap CI of share_between_individual by resampling samples."""
73
+ rng = np.random.default_rng(seed)
74
+ gs = group_stats(df, x, y, by)
75
+ samples = gs["sample"].unique()
76
+ shares = []
77
+ for _ in range(n_boot):
78
+ draw = rng.choice(samples, len(samples), replace=True)
79
+ rep = pd.DataFrame({"sample": draw}).groupby("sample").size().rename("mult").reset_index()
80
+ g2 = gs.merge(rep, on="sample")
81
+ for c in ["n", "sx", "sy", "sxy"]:
82
+ g2[c] = g2[c] * g2.mult
83
+ shares.append(partition_from_gs(g2)["share_between_individual"])
84
+ lo, hi = np.nanpercentile(shares, [2.5, 97.5])
85
+ return float(lo), float(hi)
@@ -0,0 +1,52 @@
1
+ """gating_metrics.platform_contrast — same-compartment cross-platform coupling
2
+ contrast (the H3 natural-experiment test) and a measurement-condition
3
+ compatibility report builder (frozen F1/F2 dictionary).
4
+ """
5
+ import numpy as np
6
+ from scipy import stats
7
+ import pandas as pd
8
+
9
+
10
+ def fisher_z_contrast(r1, n1, r2, n2):
11
+ """Fisher-Z test of two independent correlations (e.g. bulk-array vs
12
+ scRNA-pseudobulk coupling measured in the same biological compartment)."""
13
+ z = (np.arctanh(r1) - np.arctanh(r2)) / np.sqrt(1 / (n1 - 3) + 1 / (n2 - 3))
14
+ p = 2 * (1 - stats.norm.cdf(abs(z)))
15
+ return dict(fisher_z=float(z), p=float(p), sign_reversal=bool(r1 * r2 < 0))
16
+
17
+
18
+ def compatibility_report(rows):
19
+ """Build the measurement-conditions range table (F2).
20
+
21
+ rows: list of dicts with keys dataset, platform, compartment, state_class,
22
+ stratum, r, p, n. Classification rule (frozen): DOMINANT-NEGATIVE
23
+ r<=-0.2 & p<0.05; DOMINANT-POSITIVE r>=+0.2 & p<0.05; else
24
+ INDETERMINATE.
25
+ """
26
+ df = pd.DataFrame(rows)
27
+ if len(df) == 0:
28
+ return df
29
+
30
+ def classify(r, p):
31
+ if not np.isfinite(r) or not np.isfinite(p):
32
+ return "NON-EVALUABLE"
33
+ if r <= -0.2 and p < 0.05:
34
+ return "DOMINANT-NEGATIVE"
35
+ if r >= 0.2 and p < 0.05:
36
+ return "DOMINANT-POSITIVE"
37
+ return "INDETERMINATE"
38
+ df["classification"] = [classify(r, p) for r, p in zip(df.r, df.p)]
39
+ return df
40
+
41
+
42
+ def atlas_verdict(report, condition_filter):
43
+ """Frozen F2 atlas-level verdict: the individual-level axis principle is
44
+ supported at atlas level iff DOMINANT-NEGATIVE holds in >=80% of evaluable
45
+ cohorts of the given condition class (e.g. acute-inflammation blood)."""
46
+ sub = report[report.state_class.isin(condition_filter)]
47
+ sub = sub[sub.classification != "NON-EVALUABLE"]
48
+ if len(sub) == 0:
49
+ return dict(k=0, n=0, fraction=np.nan, supported=False)
50
+ k = int((sub.classification == "DOMINANT-NEGATIVE").sum())
51
+ frac = k / len(sub)
52
+ return dict(k=k, n=int(len(sub)), fraction=frac, supported=bool(frac >= 0.8))
@@ -0,0 +1,29 @@
1
+ """gating_metrics.power — closed-form power/sample-size for detecting a
2
+ reference-stratum Spearman coupling (the gating-metric planning tool).
3
+ """
4
+ import numpy as np
5
+ from scipy import stats
6
+
7
+
8
+ def n_for_coupling(r_target, alpha=0.05, power=0.8, two_sided=True):
9
+ """Minimum n to detect a true Spearman r at Fisher-Z (normal approx)."""
10
+ z_a = stats.norm.ppf(1 - alpha / 2 if two_sided else 1 - alpha)
11
+ z_b = stats.norm.ppf(power)
12
+ zr = np.arctanh(r_target)
13
+ return int(np.ceil((z_a + z_b) ** 2 / zr ** 2 + 3))
14
+
15
+
16
+ def power_at_n(r_true, n, alpha=0.05, two_sided=True):
17
+ """Power of the Fisher-Z test for a true correlation r at sample size n."""
18
+ z_a = stats.norm.ppf(1 - alpha / 2 if two_sided else 1 - alpha)
19
+ se = 1 / np.sqrt(n - 3)
20
+ zr = np.arctanh(r_true)
21
+ return float(stats.norm.cdf(abs(zr) / se - z_a))
22
+
23
+
24
+ def mde_at_n(n, alpha=0.05, power=0.8, two_sided=True):
25
+ """Minimum detectable |r| at given n."""
26
+ z_a = stats.norm.ppf(1 - alpha / 2 if two_sided else 1 - alpha)
27
+ z_b = stats.norm.ppf(power)
28
+ r = np.tanh((z_a + z_b) / np.sqrt(n - 3))
29
+ return float(r)
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python
2
+ """gating_metrics self-tests (frozen F3 deliverable): run with
3
+ python test_gating_metrics.py
4
+ Asserts (i) the method-of-moments partition equals the direct algebraic
5
+ identity on a synthetic two-level dataset (seed 42), (ii) the closed-form
6
+ power calculator reproduces the standard Fisher-Z n formula, (iii) the
7
+ platform Fisher-Z contrast recovers a known sign reversal, and (iv) the
8
+ compatibility-report classifier matches the frozen thresholds.
9
+ """
10
+ import os, sys
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
+ from partition import partition, bootstrap_share_between
16
+ from power import n_for_coupling, power_at_n, mde_at_n
17
+ from platform_contrast import fisher_z_contrast, compatibility_report, atlas_verdict
18
+ from coupling import spearman_np, partial_spearman
19
+
20
+
21
+ def make_two_level(seed=42, n_samples=60, cells_per=40, r_between=-0.6,
22
+ r_within=0.0):
23
+ rng = np.random.default_rng(seed)
24
+ mus = rng.multivariate_normal([0, 0], [[1, r_between], [r_between, 1]],
25
+ size=n_samples)
26
+ rows = []
27
+ for s, (mx, my) in enumerate(mus):
28
+ xy = rng.multivariate_normal([mx, my], [[1, r_within], [r_within, 1]],
29
+ size=cells_per)
30
+ for x, y in xy:
31
+ rows.append({"sample": f"S{s}", "x": x, "y": y})
32
+ return pd.DataFrame(rows)
33
+
34
+
35
+ def main():
36
+ # (i) partition identity + between-individual share recovery
37
+ df = make_two_level()
38
+ p = partition(df[["sample", "x", "y"]], by=("sample",))
39
+ assert abs(p["SSP_tot"] - p["SSP_between_individual"] - p["SSP_within"]) \
40
+ < 1e-6 * max(1.0, abs(p["SSP_tot"]))
41
+ lo, hi = bootstrap_share_between(df[["sample", "x", "y"]], by=("sample",),
42
+ n_boot=200)
43
+ assert lo < p["share_between_individual"] < hi, (lo, p, hi)
44
+ print(f"partition OK: share_between={p['share_between_individual']:.3f} "
45
+ f"bootstrap95=[{lo:.3f},{hi:.3f}]")
46
+
47
+ # (ii) power calculators
48
+ n_req = n_for_coupling(0.3)
49
+ assert power_at_n(0.3, n_req) >= 0.8 - 1e-6
50
+ assert abs(mde_at_n(n_req) - 0.3) < 0.02
51
+ print(f"power OK: n(r=0.3, 80%)={n_req}, MDE at that n={mde_at_n(n_req):.3f}")
52
+
53
+ # (iii) Fisher-Z platform contrast with known sign reversal
54
+ c = fisher_z_contrast(-0.5, 40, +0.4, 200)
55
+ assert c["sign_reversal"] and c["p"] < 0.05
56
+ print(f"fisher-z OK: p={c['p']:.2e}, sign_reversal={c['sign_reversal']}")
57
+
58
+ # (iv) classification thresholds + atlas verdict
59
+ rows = [
60
+ dict(dataset="A", platform="array", compartment="whole_blood",
61
+ state_class="acute_blood", stratum="ref", r=-0.4, p=0.01, n=50),
62
+ dict(dataset="B", platform="array", compartment="whole_blood",
63
+ state_class="acute_blood", stratum="ref", r=-0.3, p=0.02, n=80),
64
+ dict(dataset="C", platform="scRNA", compartment="PBMC",
65
+ state_class="acute_blood", stratum="ref", r=+0.3, p=0.03, n=100),
66
+ dict(dataset="D", platform="array", compartment="whole_blood",
67
+ state_class="acute_blood", stratum="ref", r=-0.1, p=0.4, n=30),
68
+ ]
69
+ rep = compatibility_report(rows)
70
+ v = atlas_verdict(rep, ["acute_blood"])
71
+ assert v["k"] == 2 and v["n"] == 4 and not v["supported"]
72
+ print("classification OK:", dict(zip(rep.dataset, rep.classification)))
73
+ print("ALL gating_metrics SELF-TESTS PASSED")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: gating-metrics
3
+ Version: 0.1.0
4
+ Summary: Measurement toolkit for state-gated co-expression coupling: layer couplings, exact covariance partition, platform contrast, closed-form power planning
5
+ Author: Liu Yuanshui
6
+ License: MIT
7
+ Project-URL: OSF registration, https://doi.org/10.17605/OSF.IO/4TZH9
8
+ Project-URL: OSF project, https://osf.io/v8w5a
9
+ Project-URL: Homepage, https://github.com/Shuiruolys-Liu/gating-metrics
10
+ Keywords: co-expression,single-cell,gating,ARDS,inflammasome,measurement
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: pandas>=2.0
20
+ Requires-Dist: scipy>=1.10
21
+ Dynamic: license-file
22
+
23
+ # gating_metrics — measurement toolkit for state-gated co-expression coupling
24
+
25
+ Open-source Python toolkit accompanying the ARDS gating-circuit study
26
+ (manifest modules F1/F2/F3, registered 2026-08-18 before execution). It
27
+ formalises the study's core measurement claim — that a state-gated
28
+ hub–module coupling (e.g. KAT2A vs the inflammasome module) is an
29
+ **individual-level, platform-dependent emergent measurement** — into
30
+ reusable, tested routines.
31
+
32
+ ## What it provides
33
+
34
+ | Module | Contents |
35
+ |---|---|
36
+ | `coupling.py` | Layer-wise Spearman coupling: `L_cell` (within-sample per-cell, with detected-gene-count partial sensitivity), `L_indiv` (between-individual pseudobulk), `L_bulk` (bulk-assay). |
37
+ | `partition.py` | Exact method-of-moments (nested ANOVA cross-product) covariance partition: between-individual / between-compartment-within-individual / within components, with sample-level bootstrap CIs. |
38
+ | `platform_contrast.py` (renamed from `platform.py` to avoid the stdlib collision) | Fisher-Z same-compartment cross-platform contrast (the "is it a platform artefact?" test) and the measurement-conditions compatibility report / atlas verdict (frozen classification thresholds: DOMINANT-NEGATIVE r≤−0.2 & p<0.05; DOMINANT-POSITIVE r≥+0.2 & p<0.05; else INDETERMINATE; atlas support = ≥80% DOMINANT-NEGATIVE within a condition class). |
39
+ | `power.py` | Closed-form Fisher-Z planning tools: n for a target coupling, power at n, minimum detectable |r|. |
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import pandas as pd
45
+ from coupling import layer_cell, layer_indiv
46
+ from partition import partition, bootstrap_share_between
47
+
48
+ # cells: one row per cell with columns sample, compartment, x (hub gene),
49
+ # y (module score), ng (detected-gene count)
50
+ per_group, summary = layer_cell(cells, ["sample", "compartment"])
51
+ r_indiv, p_indiv = layer_indiv(pseudobulk) # one row per sample
52
+ part = partition(cells, by=("sample", "compartment"))
53
+ lo, hi = bootstrap_share_between(cells, by=("sample", "compartment"))
54
+ ```
55
+
56
+ Self-tests (synthetic two-level data, seed 42; partition identity, power
57
+ closed form, Fisher-Z sign reversal, classification thresholds):
58
+
59
+ ```
60
+ python test_gating_metrics.py
61
+ ```
62
+
63
+ ## Registered falsifiable prediction (public test for the field)
64
+
65
+ Any new **acute-inflammation whole-blood cohort assayed in bulk** (array or
66
+ bulk RNA-seq) should reproduce a **negative reference-stratum hub–module
67
+ coupling**; **scRNA pseudobulk and protein layers are expected non-negative**
68
+ (same compartment). The F1/F2 tables in the companion manuscript are the
69
+ current evidence base; the prediction is falsifiable by any cohort that
70
+ violates the sign-by-condition pattern.
71
+
72
+ **Public registration**: OSF Preregistration, 2026-08-18 —
73
+ DOI [10.17605/OSF.IO/4TZH9](https://doi.org/10.17605/OSF.IO/4TZH9)
74
+ (associated project https://osf.io/v8w5a).
75
+
76
+ **External validation status (recorded 2026-08-19, honest)**: the authors'
77
+ own value-blind external hold-out (module G1; four never-used
78
+ acute-inflammation blood cohorts GSE66099/GSE40012/GSE57065/GSE37069)
79
+ did **NOT** confirm the reference-stratum prediction (2/4 direction-negative;
80
+ pooled fixed-effect r=+0.004). The registered text above is kept unchanged as
81
+ the auditable record; by the external data, the reference-stratum claim is
82
+ refuted and the inflamed-stratum negative coupling is supported (manuscript
83
+ TableS36). Independent replication by other groups remains the intended
84
+ falsification channel.
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install gating-metrics # once published on PyPI (pending author action)
90
+ # or from source:
91
+ git clone https://github.com/Shuiruolys-Liu/gating-metrics
92
+ cd gating-metrics && pip install .
93
+ ```
94
+
95
+ ## Provenance
96
+
97
+ Metric definitions are copied verbatim from the frozen F1 decomposition
98
+ dictionary (`manifest.json -> preregistrations.modules.
99
+ F1_variance_decomposition_gating`, registered 2026-08-18). No metric in this
100
+ package was tuned after inspecting the outcome data.
101
+
102
+ ## Citing
103
+
104
+ If you use this toolkit, please cite the companion manuscript (under
105
+ preparation) and the OSF registration (DOI 10.17605/OSF.IO/4TZH9).
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ gating_metrics/__init__.py
5
+ gating_metrics/coupling.py
6
+ gating_metrics/partition.py
7
+ gating_metrics/platform_contrast.py
8
+ gating_metrics/power.py
9
+ gating_metrics/test_gating_metrics.py
10
+ gating_metrics.egg-info/PKG-INFO
11
+ gating_metrics.egg-info/SOURCES.txt
12
+ gating_metrics.egg-info/dependency_links.txt
13
+ gating_metrics.egg-info/requires.txt
14
+ gating_metrics.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy>=1.24
2
+ pandas>=2.0
3
+ scipy>=1.10
@@ -0,0 +1 @@
1
+ gating_metrics
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gating-metrics"
7
+ version = "0.1.0"
8
+ description = "Measurement toolkit for state-gated co-expression coupling: layer couplings, exact covariance partition, platform contrast, closed-form power planning"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Liu Yuanshui" }]
13
+ keywords = ["co-expression", "single-cell", "gating", "ARDS", "inflammasome", "measurement"]
14
+ classifiers = [
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
19
+ ]
20
+ dependencies = ["numpy>=1.24", "pandas>=2.0", "scipy>=1.10"]
21
+
22
+ [project.urls]
23
+ "OSF registration" = "https://doi.org/10.17605/OSF.IO/4TZH9"
24
+ "OSF project" = "https://osf.io/v8w5a"
25
+ Homepage = "https://github.com/Shuiruolys-Liu/gating-metrics"
26
+
27
+ [tool.setuptools]
28
+ packages = ["gating_metrics"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+