pooledscreenid 0.1.1__py3-none-any.whl

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,14 @@
1
+ """Public API for PooledScreenID."""
2
+
3
+ from .audit import evaluate_evidence, run_audit, summarize_fold_gains
4
+ from .models import AuditConfig, EvidenceVector
5
+
6
+ __all__ = [
7
+ "AuditConfig",
8
+ "EvidenceVector",
9
+ "evaluate_evidence",
10
+ "run_audit",
11
+ "summarize_fold_gains",
12
+ ]
13
+
14
+ __version__ = "0.1.1"
@@ -0,0 +1,186 @@
1
+ """Evidence-vector construction and priority-ordered claim resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+ from statistics import mean
10
+ from typing import Any
11
+
12
+ from .ledger import build_claim_ledger
13
+ from .models import AuditConfig, EvidenceVector
14
+
15
+
16
+ def sha256_file(path: str | Path) -> str:
17
+ target = Path(path)
18
+ return hashlib.sha256(target.read_bytes()).hexdigest()
19
+
20
+
21
+ def summarize_fold_gains(path: str | Path) -> dict[str, Any]:
22
+ """Summarize a frozen fold table.
23
+
24
+ Required columns are `fold`, `unadjusted_gain` and `adjusted_gain`.
25
+ Folds are descriptive repetitions, not independent biological units.
26
+ """
27
+ target = Path(path)
28
+ with target.open(newline="", encoding="utf-8") as handle:
29
+ rows = list(csv.DictReader(handle))
30
+ if not rows:
31
+ raise ValueError("fold-gain table is empty")
32
+ required = {"fold", "unadjusted_gain", "adjusted_gain"}
33
+ missing = required - set(rows[0])
34
+ if missing:
35
+ raise ValueError(f"missing fold columns: {sorted(missing)}")
36
+ folds = [row["fold"] for row in rows]
37
+ if len(folds) != len(set(folds)):
38
+ raise ValueError("fold identifiers must be unique")
39
+ unadjusted = [float(row["unadjusted_gain"]) for row in rows]
40
+ adjusted = [float(row["adjusted_gain"]) for row in rows]
41
+ return {
42
+ "folds": len(rows),
43
+ "unadjusted_gain_mean": mean(unadjusted),
44
+ "adjusted_gain_mean": mean(adjusted),
45
+ "unadjusted_gain_range": [min(unadjusted), max(unadjusted)],
46
+ "adjusted_gain_range": [min(adjusted), max(adjusted)],
47
+ "input_sha256": sha256_file(target),
48
+ }
49
+
50
+
51
+ def evaluate_evidence(config: AuditConfig, summary: dict[str, Any]) -> EvidenceVector:
52
+ threshold = config.materiality_threshold
53
+ unadjusted = float(summary["unadjusted_gain_mean"])
54
+ adjusted = float(summary["adjusted_gain_mean"])
55
+ material = unadjusted >= threshold
56
+ control_associated = material and adjusted < threshold
57
+
58
+ distance: float | None = None
59
+ if config.eta is None or config.eta_boundary is None:
60
+ separability = "not_evaluated"
61
+ else:
62
+ distance = config.eta_boundary - config.eta
63
+ if config.eta > config.eta_boundary:
64
+ separability = "non_identifiable"
65
+ elif distance <= config.boundary_zone:
66
+ separability = "boundary_proximal"
67
+ else:
68
+ separability = "within_calibrated_region"
69
+
70
+ external_status = str(config.external_calibration.get("status", "not_evaluated"))
71
+ if separability == "non_identifiable":
72
+ resolution = "non_identifiable"
73
+ elif separability == "boundary_proximal":
74
+ resolution = "restricted_boundary_proximal"
75
+ elif not material:
76
+ resolution = "no_material_increment"
77
+ elif control_associated:
78
+ resolution = "control_associated"
79
+ elif external_status == "supportive_same_estimand":
80
+ resolution = "externally_supported_surviving_increment"
81
+ else:
82
+ resolution = "surviving_increment"
83
+
84
+ allowed = [
85
+ f"The frozen {config.estimand} and its uncertainty may be reported.",
86
+ "Cold-start transport and information-source attribution are reported separately.",
87
+ ]
88
+ restricted: list[str] = []
89
+ prohibited = [
90
+ "Prediction alone does not establish a binding or causal mechanism.",
91
+ "This analysis does not support patient-level treatment recommendation.",
92
+ ]
93
+ if not material:
94
+ restricted.append("No material increment was observed at the frozen threshold.")
95
+ prohibited.append("Do not interpret the result as equivalence or a biological null.")
96
+ if control_associated:
97
+ restricted.append("The apparent increment is associated with the outcome-blind control channel.")
98
+ prohibited.append("Do not claim that adjustment proves the shared signal is purely technical.")
99
+ if separability in {"non_identifiable", "boundary_proximal"}:
100
+ restricted.append("Fine-grained attribution is restricted by the prespecified separability assessment.")
101
+ prohibited.append("Do not make a structure-specific attribution from this dataset.")
102
+ if separability == "not_evaluated":
103
+ prohibited.append("Do not claim that representation separability was established.")
104
+ if external_status == "supportive_distinct_estimand":
105
+ restricted.append(
106
+ "External positive calibration concerns a distinct estimand and does not validate the target representation."
107
+ )
108
+ elif external_status in {"not_evaluated", "external_contrast_only"}:
109
+ prohibited.append("Do not claim external calibration of the target estimand.")
110
+
111
+ context = {
112
+ "prediction_unit": config.prediction_unit,
113
+ "independent_unit": config.independent_unit,
114
+ "block_unit": config.block_unit,
115
+ "target_representation": config.target_representation,
116
+ "outcome_blind_control": config.outcome_blind_control,
117
+ "source_id": config.source_id,
118
+ "fold_summary": summary,
119
+ "folds_are_independent_biological_units": False,
120
+ }
121
+ return EvidenceVector(
122
+ dataset_id=config.dataset_id,
123
+ folds=int(summary["folds"]),
124
+ unadjusted_gain_mean=unadjusted,
125
+ adjusted_gain_mean=adjusted,
126
+ materiality_threshold=threshold,
127
+ material_signal="supported" if material else "below_threshold",
128
+ control_association="supported" if control_associated else "not_supported",
129
+ separability=separability,
130
+ external_calibration=config.external_calibration,
131
+ eta=config.eta,
132
+ eta_boundary=config.eta_boundary,
133
+ boundary_distance=distance,
134
+ claim_resolution=resolution,
135
+ allowed_claims=tuple(allowed),
136
+ restricted_claims=tuple(restricted),
137
+ prohibited_claims=tuple(prohibited),
138
+ audit_context=context,
139
+ )
140
+
141
+
142
+ def _markdown(report: dict[str, Any]) -> str:
143
+ return "\n".join(
144
+ [
145
+ f"# {report['dataset_id']} PooledScreenID report",
146
+ "",
147
+ f"- Claim resolution: `{report['claim_resolution']}`",
148
+ f"- Unadjusted gain: `{report['unadjusted_gain_mean']:.6f}`",
149
+ f"- Adjusted gain: `{report['adjusted_gain_mean']:.6f}`",
150
+ f"- Materiality threshold: `{report['materiality_threshold']:.6f}`",
151
+ f"- Separability: `{report['separability']}`",
152
+ f"- External calibration: `{report['external_calibration']['status']}`",
153
+ "",
154
+ "## Allowed claims",
155
+ *[f"- {x}" for x in report["allowed_claims"]],
156
+ "",
157
+ "## Restricted claims",
158
+ *([f"- {x}" for x in report["restricted_claims"]] or ["- None recorded."]),
159
+ "",
160
+ "## Prohibited interpretations",
161
+ *[f"- {x}" for x in report["prohibited_claims"]],
162
+ "",
163
+ ]
164
+ )
165
+
166
+
167
+ def run_audit(config_path: str | Path, output_dir: str | Path) -> tuple[dict[str, Any], dict[str, Any]]:
168
+ config_file = Path(config_path).resolve()
169
+ config = AuditConfig.from_dict(json.loads(config_file.read_text(encoding="utf-8")))
170
+ fold_file = (config_file.parent / config.fold_gains_csv).resolve()
171
+ summary = summarize_fold_gains(fold_file)
172
+ vector = evaluate_evidence(config, summary)
173
+ report = vector.to_dict()
174
+ report["software"] = {"name": "pooledscreenid", "version": "0.1.1"}
175
+ report["config_sha256"] = sha256_file(config_file)
176
+ ledger = build_claim_ledger(report)
177
+ target = Path(output_dir)
178
+ target.mkdir(parents=True, exist_ok=True)
179
+ (target / "report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
180
+ (target / "report.md").write_text(_markdown(report), encoding="utf-8")
181
+ (target / "claim_ledger.json").write_text(json.dumps(ledger, indent=2) + "\n", encoding="utf-8")
182
+ if config.expected_claim_resolution and report["claim_resolution"] != config.expected_claim_resolution:
183
+ raise RuntimeError(
184
+ f"frozen expectation mismatch: {report['claim_resolution']} != {config.expected_claim_resolution}"
185
+ )
186
+ return report, ledger
pooledscreenid/cli.py ADDED
@@ -0,0 +1,23 @@
1
+ """Command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from .audit import run_audit
9
+
10
+
11
+ def main() -> None:
12
+ parser = argparse.ArgumentParser(prog="pooledscreenid")
13
+ sub = parser.add_subparsers(dest="command", required=True)
14
+ audit = sub.add_parser("audit", help="run an evidence-to-claim audit")
15
+ audit.add_argument("--config", required=True)
16
+ audit.add_argument("--output-dir", required=True)
17
+ args = parser.parse_args()
18
+ report, _ = run_audit(args.config, args.output_dir)
19
+ print(json.dumps({"dataset_id": report["dataset_id"], "claim_resolution": report["claim_resolution"]}))
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,42 @@
1
+ """Versioned machine-readable claim ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def build_claim_ledger(report: dict[str, Any]) -> dict[str, Any]:
9
+ evidence_id = f"{report['dataset_id']}:screen-audit-v1"
10
+ return {
11
+ "schema_version": "1.1.0",
12
+ "software": report["software"],
13
+ "dataset_id": report["dataset_id"],
14
+ "evidence": [
15
+ {
16
+ "evidence_id": evidence_id,
17
+ "material_signal": report["material_signal"],
18
+ "control_association": report["control_association"],
19
+ "separability": report["separability"],
20
+ "external_calibration": report["external_calibration"],
21
+ "claim_resolution": report["claim_resolution"],
22
+ "resolution_inputs": {
23
+ "material_signal": report["material_signal"],
24
+ "control_association": report["control_association"],
25
+ "separability": report["separability"],
26
+ "external_calibration": report["external_calibration"]["status"],
27
+ },
28
+ "config_sha256": report["config_sha256"],
29
+ "input_sha256": report["audit_context"]["fold_summary"]["input_sha256"],
30
+ }
31
+ ],
32
+ "allowed_claims": [
33
+ {"text": text, "evidence_ids": [evidence_id]} for text in report["allowed_claims"]
34
+ ],
35
+ "restricted_claims": [
36
+ {"text": text, "evidence_ids": [evidence_id]} for text in report["restricted_claims"]
37
+ ],
38
+ "prohibited_claims": [
39
+ {"text": text, "evidence_ids": [evidence_id]} for text in report["prohibited_claims"]
40
+ ],
41
+ "human_review_required": True,
42
+ }
@@ -0,0 +1,64 @@
1
+ """Typed configuration and evidence objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AuditConfig:
11
+ dataset_id: str
12
+ fold_gains_csv: str
13
+ materiality_threshold: float
14
+ target_representation: str
15
+ outcome_blind_control: str
16
+ prediction_unit: str
17
+ independent_unit: str
18
+ block_unit: str
19
+ estimand: str
20
+ eta: float | None = None
21
+ eta_boundary: float | None = None
22
+ boundary_zone: float = 0.0
23
+ source_id: str = ""
24
+ external_calibration: dict[str, Any] = field(
25
+ default_factory=lambda: {
26
+ "status": "not_evaluated",
27
+ "source": "",
28
+ "estimand": "",
29
+ "note": "No external calibration was supplied for this audit.",
30
+ }
31
+ )
32
+ expected_claim_resolution: str | None = None
33
+ metadata: dict[str, Any] = field(default_factory=dict)
34
+
35
+ @classmethod
36
+ def from_dict(cls, value: dict[str, Any]) -> "AuditConfig":
37
+ return cls(**value)
38
+
39
+ def to_dict(self) -> dict[str, Any]:
40
+ return asdict(self)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class EvidenceVector:
45
+ dataset_id: str
46
+ folds: int
47
+ unadjusted_gain_mean: float
48
+ adjusted_gain_mean: float
49
+ materiality_threshold: float
50
+ material_signal: str
51
+ control_association: str
52
+ separability: str
53
+ external_calibration: dict[str, Any]
54
+ eta: float | None
55
+ eta_boundary: float | None
56
+ boundary_distance: float | None
57
+ claim_resolution: str
58
+ allowed_claims: tuple[str, ...]
59
+ restricted_claims: tuple[str, ...]
60
+ prohibited_claims: tuple[str, ...]
61
+ audit_context: dict[str, Any]
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return asdict(self)
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: pooledscreenid
3
+ Version: 0.1.1
4
+ Summary: Outcome-blind shortcut and separability diagnostics for pooled variant-effect screens
5
+ Author: Niu Niu, Fang Wei, Yan Wang, Hu Liu, Bin Wu
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://doi.org/10.5281/zenodo.22156510
8
+ Project-URL: Repository, https://doi.org/10.5281/zenodo.22156510
9
+ Keywords: deep mutational scanning,cold start,negative control,identifiability,variant effect
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # PooledScreenID
23
+
24
+ PooledScreenID is a small, installable Python package for reporting four
25
+ quantities separately in cold-start pooled variant-effect analyses:
26
+
27
+ 1. material predictive gain;
28
+ 2. association with an outcome-blind control channel;
29
+ 3. representation separability at a prespecified operating scale;
30
+ 4. external calibration, kept distinct from the target estimand; and
31
+ 5. the resulting allowed, restricted and prohibited claims.
32
+
33
+ The package does not infer a molecular mechanism and does not make treatment
34
+ recommendations. A cold split is treated as a transport test, not as proof of
35
+ the information source used by a model.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ python -m pip install pooledscreenid-0.1.1-py3-none-any.whl
41
+ ```
42
+
43
+ For a source checkout:
44
+
45
+ ```bash
46
+ python -m pip install -e .
47
+ ```
48
+
49
+ ## Minimal Python API
50
+
51
+ ```python
52
+ from pooledscreenid import run_audit
53
+
54
+ report, ledger = run_audit(
55
+ config_path="examples/configs/egfr.json",
56
+ output_dir="example_output/egfr",
57
+ )
58
+ print(report["claim_resolution"])
59
+ ```
60
+
61
+ ## Command line
62
+
63
+ ```bash
64
+ pooledscreenid audit \
65
+ --config examples/configs/egfr.json \
66
+ --output-dir example_output/egfr
67
+
68
+ pooledscreenid audit \
69
+ --config examples/configs/met.json \
70
+ --output-dir example_output/met
71
+ ```
72
+
73
+ Each run writes `report.json`, `report.md` and `claim_ledger.json`. Inputs,
74
+ configuration, source hashes and expected outputs are frozen in the release.
75
+ The ledger exposes `material_signal`, `control_association`, `separability` and
76
+ `external_calibration` as independent fields. `claim_resolution` is generated
77
+ from those fields under priority-ordered rules and never replaces them.
78
+
79
+ PooledScreenID 0.1.1 is the article-linked frozen release. The public API is
80
+ managed with semantic versioning. It supports Python 3.10-3.13 and has no
81
+ third-party runtime dependencies; the test suite uses pytest.
82
+
83
+ ## Included end-to-end examples
84
+
85
+ - `EGFR`: ten frozen variant-cold folds from the L858R-background pooled EGFR
86
+ inhibitor screen, plus the outcome-free geometry and conditional operating
87
+ boundary used in the manuscript.
88
+ - `MET`: ten frozen variant-cold folds from an independently selected pooled
89
+ MET inhibitor screen. The example demonstrates a result below the materiality
90
+ gate and explicitly leaves separability unevaluated.
91
+
92
+ These examples begin with frozen, source-derived fold diagnostics rather than
93
+ raw sequencing reads. They reproduce the evidence-to-claim stage of the
94
+ framework; raw count processing and model fitting remain documented in the
95
+ accompanying analysis repository.
96
+
97
+ ## Interpretation order
98
+
99
+ The software reports the four-axis evidence vector before returning a summary label. If
100
+ shared predictability exceeds its prespecified boundary, or lies inside a
101
+ prespecified boundary zone, fine-grained attribution is restricted before any
102
+ control-specific contrast is interpreted. Absence of a material increment is
103
+ not an equivalence or biological-null claim.
104
+
105
+ ## Release integrity
106
+
107
+ `RELEASE_MANIFEST.json` and `SHA256SUMS.txt` bind the software, tests, continuous-integration configuration, examples,
108
+ configuration files and claim ledger to this release. The Zenodo record is the
109
+ permanent release series: <https://doi.org/10.5281/zenodo.22156510>.
@@ -0,0 +1,11 @@
1
+ pooledscreenid/__init__.py,sha256=9o0NnQxjPSiEPOkB9CSV8WQ4a6mjQk92ps2c4VZb67I,305
2
+ pooledscreenid/audit.py,sha256=b2p55RmTYJWwQ5gGv2Cx2MsIYB0qIGJtlCP0Rs_uOh4,8092
3
+ pooledscreenid/cli.py,sha256=HhIgkN4tyI5ekIT1b4XgAsdIkBQYlPqqj4IE_FOGrRI,684
4
+ pooledscreenid/ledger.py,sha256=Mj0jD49eyXyPrTb_Ob4VH6AkRDFt_wg51lXAsuSdGxA,1746
5
+ pooledscreenid/models.py,sha256=p7tUH6kJ0xbHHyV4e5Tb7DHxia6o1BdRIBjEI0Et1A4,1737
6
+ pooledscreenid-0.1.1.dist-info/licenses/LICENSE,sha256=LLLMBRn7oZ4PwrMz6U8UCSVYRigdirt1qhSA6mP7ju0,1084
7
+ pooledscreenid-0.1.1.dist-info/METADATA,sha256=tZFvDpwtXnWJdX_jtAn4adM1Br5fmAbQyHpKujTZ0xA,4236
8
+ pooledscreenid-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ pooledscreenid-0.1.1.dist-info/entry_points.txt,sha256=yf1FXDIoXcQunmZ58K4UJO4nuO7LsTcSELBm6TEvAqU,59
10
+ pooledscreenid-0.1.1.dist-info/top_level.txt,sha256=j6eeGZFsoHs0t_XDOw04BjRL8Tl_otA3G0bpwccAtTQ,15
11
+ pooledscreenid-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pooledscreenid = pooledscreenid.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PooledScreenID contributors
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 @@
1
+ pooledscreenid