orfdata 0.3.0__tar.gz → 0.4.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.
- {orfdata-0.3.0/orfdata.egg-info → orfdata-0.4.0}/PKG-INFO +1 -1
- {orfdata-0.3.0 → orfdata-0.4.0}/docs/measurements.md +3 -3
- orfdata-0.4.0/docs/pairing.md +84 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/__init__.py +14 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/dataset.py +118 -4
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/dataset_cli.py +17 -1
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/measurements.py +1 -0
- orfdata-0.4.0/orfdata/pairing.py +329 -0
- orfdata-0.4.0/orfdata/version.py +1 -0
- {orfdata-0.3.0 → orfdata-0.4.0/orfdata.egg-info}/PKG-INFO +1 -1
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata.egg-info/SOURCES.txt +4 -0
- orfdata-0.4.0/tests/conftest.py +45 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/test_measurements.py +2 -33
- orfdata-0.4.0/tests/test_pairing.py +269 -0
- orfdata-0.3.0/orfdata/version.py +0 -1
- {orfdata-0.3.0 → orfdata-0.4.0}/LICENSE +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/MANIFEST.in +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/README.md +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/deploy.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/docs/design.md +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/docs/reference.md +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/docs/species.md +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/lint.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/build.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/catalog.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/cli.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/datasets.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/orf-evidence/manifest.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/datasets.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/initiation_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/model.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/reference.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata/sequence.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata.egg-info/dependency_links.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata.egg-info/entry_points.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata.egg-info/requires.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/orfdata.egg-info/top_level.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/pyproject.toml +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/setup.cfg +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/test.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/data/references/README.md +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/data/references/homo_sapiens.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/data/references/mus_musculus.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/test_initiation_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/test_reference_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/test_reference_orf.py +0 -0
- {orfdata-0.3.0 → orfdata-0.4.0}/tests/test_uorf_catalog.py +0 -0
|
@@ -80,9 +80,9 @@ records technical replicates, runs, fractions, TMT plex/channel/bridge,
|
|
|
80
80
|
normalization and calibration. A sample's strain/breed belongs in its attributes;
|
|
81
81
|
it need not be the strain used to assemble its reference genome. `TargetMapping`
|
|
82
82
|
retains all candidate assignments or an unmapped reason. A protein-group
|
|
83
|
-
representative does not become a uniquely measured protein.
|
|
84
|
-
pairing and orthology comparisons
|
|
85
|
-
|
|
83
|
+
representative does not become a uniquely measured protein. See
|
|
84
|
+
[pairing and orthology comparisons](pairing.md) for explicit experimental
|
|
85
|
+
relationships and derived RNA/protein ratios.
|
|
86
86
|
|
|
87
87
|
## Importing a source
|
|
88
88
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Comparing RNA, ribosome and protein abundance
|
|
2
|
+
|
|
3
|
+
Use declared experimental relationships to compare assays. A tissue label alone
|
|
4
|
+
does not make an RNA sample and a protein sample a matched pair.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
for match in dataset.matched_measurements(allowed_bases=["same_material"]):
|
|
8
|
+
row = match.to_table_row()
|
|
9
|
+
print(row["rna_abundance"], row["protein_abundance"])
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Rows include named RNA, ribosome occupancy, translation-efficiency, protein,
|
|
13
|
+
peptide and spectrum-count columns, each containing original measurement
|
|
14
|
+
records with units, missingness and provenance. Missing columns are empty lists.
|
|
15
|
+
Multiple values remain multiple values; the API does not average them or create
|
|
16
|
+
a Cartesian product of technical replicates. Each row retains its assay pairing
|
|
17
|
+
and target relationship. A three-assay experiment can declare several pairwise
|
|
18
|
+
comparisons; sharing an RNA measurement does not create independent replicates.
|
|
19
|
+
|
|
20
|
+
Pairing bases are `same_material`, `adjacent_donor_tissue`,
|
|
21
|
+
`biological_replicate`, `independent_cultures`, `tissue_aggregate` and
|
|
22
|
+
`condition_aggregate`. The caller must choose the allowed bases. By default,
|
|
23
|
+
only explicitly concurrent assays are returned; opt into `longitudinal` or
|
|
24
|
+
`unknown` with `allowed_temporal`. Missing mates stay explicit. Technical runs,
|
|
25
|
+
TMT channels and bridges remain in the assays and do not become biological
|
|
26
|
+
sample counts.
|
|
27
|
+
|
|
28
|
+
The CLI streams the same table rows:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
orfdata datasets pairs DATASET --version RELEASE --basis same_material
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Which targets are being compared?
|
|
35
|
+
|
|
36
|
+
`TargetRelation` links exact, reference-scoped targets with mapping evidence
|
|
37
|
+
and cardinality. `same_target` compares one target across assays/samples.
|
|
38
|
+
`canonical_translation` links a gene/transcript to its annotated protein.
|
|
39
|
+
`parent_rna_context` links parent RNA to an ORF but keeps the RNA value attached
|
|
40
|
+
to its original gene/transcript. `shared_group_member` records an ambiguous
|
|
41
|
+
protein-group assignment; the group value stays a group value.
|
|
42
|
+
|
|
43
|
+
Importers pass `AssayPair` and `TargetRelation` iterators as `pairings=` and
|
|
44
|
+
`relations=` to `QuantitativeDataset.create`. IDs belong to the dataset release
|
|
45
|
+
and reference context. Invalid assay references, contradictory known donor
|
|
46
|
+
identities and conflicting one-to-one assignments fail the import. Original
|
|
47
|
+
storage-version-1 datasets remain readable and have no declared relationships.
|
|
48
|
+
|
|
49
|
+
## Ratios are derived measurements
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
ratio = match.ratio(
|
|
53
|
+
selected_protein_measurement, selected_rna_measurement,
|
|
54
|
+
measurement_id="ratio-1", method="native protein/RNA ratio",
|
|
55
|
+
normalization={"protein": "source bridge normalization", "rna": "source TPM"},
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Select actual measurements from opposite sides of the match. Ratios require
|
|
60
|
+
one-to-one target attribution and linear inputs. Parent-RNA/ORF and shared-group
|
|
61
|
+
links cannot produce target-specific ratios. There is no implicit log conversion,
|
|
62
|
+
unit conversion, replicate averaging or uncertainty propagation.
|
|
63
|
+
|
|
64
|
+
An iBAQ/TPM ratio keeps `(iBAQ)/(TPM)` units. Copies-per-cell values also retain
|
|
65
|
+
their native composite units and the calibration recorded on their source
|
|
66
|
+
assays. Missing, censored or nonpositive denominators produce an unresolved
|
|
67
|
+
`not_measured` result with its reason, never infinity or a measured zero. RPF/RNA
|
|
68
|
+
ratios are source-defined translation-efficiency measures, not measured flux.
|
|
69
|
+
All derived values retain their input IDs, pairing, target attribution and
|
|
70
|
+
normalization assumptions.
|
|
71
|
+
|
|
72
|
+
## Across species
|
|
73
|
+
|
|
74
|
+
`compare_orthologs(left_dataset, right_dataset, mappings)` is separate and
|
|
75
|
+
opt-in. Each `OrthologyMapping` pins its source file, release, both reference
|
|
76
|
+
contexts, cardinality, confidence definition and evidence. The default accepts
|
|
77
|
+
one-to-one canonical gene mappings; one-to-many relationships require explicit
|
|
78
|
+
permission through `allowed_cardinalities`.
|
|
79
|
+
|
|
80
|
+
The result contains each species' original observations. It neither merges
|
|
81
|
+
identities nor transfers abundance or protein-existence evidence. ORF comparisons
|
|
82
|
+
additionally require their own sequence, frame and positional evidence and
|
|
83
|
+
`allow_orfs=True`; a gene ortholog cannot establish an orthologous ncORF.
|
|
84
|
+
Human, mouse, dog, yeast and arbitrary other taxa use this same API.
|
|
@@ -17,6 +17,14 @@ from .datasets import DatasetRegistry, build_dataset
|
|
|
17
17
|
from .initiation_context import KOZAK_PREFERENCES, InitiationContext, annotate_initiation_context
|
|
18
18
|
from .measurements import Assay, DatasetManifest, Measurement, Sample, SourceAsset, TargetMapping
|
|
19
19
|
from .model import ORFModel, ReferenceORF
|
|
20
|
+
from .pairing import (
|
|
21
|
+
AssayPair,
|
|
22
|
+
MatchedMeasurements,
|
|
23
|
+
OrthologyComparison,
|
|
24
|
+
OrthologyMapping,
|
|
25
|
+
TargetRelation,
|
|
26
|
+
compare_orthologs,
|
|
27
|
+
)
|
|
20
28
|
from .reference import ReferenceContext, ScopedTarget
|
|
21
29
|
from .version import __version__
|
|
22
30
|
|
|
@@ -40,6 +48,12 @@ __all__ = [
|
|
|
40
48
|
"Assay",
|
|
41
49
|
"Measurement",
|
|
42
50
|
"TargetMapping",
|
|
51
|
+
"AssayPair",
|
|
52
|
+
"TargetRelation",
|
|
53
|
+
"MatchedMeasurements",
|
|
54
|
+
"OrthologyMapping",
|
|
55
|
+
"OrthologyComparison",
|
|
56
|
+
"compare_orthologs",
|
|
43
57
|
"load_catalog",
|
|
44
58
|
"UORFCatalog",
|
|
45
59
|
"UORFRecord",
|
|
@@ -16,6 +16,14 @@ from .measurements import (
|
|
|
16
16
|
Sample,
|
|
17
17
|
TargetMapping,
|
|
18
18
|
)
|
|
19
|
+
from .pairing import (
|
|
20
|
+
PAIRING_BASES,
|
|
21
|
+
TEMPORAL_RELATIONS,
|
|
22
|
+
AssayPair,
|
|
23
|
+
MatchedMeasurements,
|
|
24
|
+
TargetRelation,
|
|
25
|
+
_check_one_to_one,
|
|
26
|
+
)
|
|
19
27
|
from .reference import ReferenceContext, ScopedTarget
|
|
20
28
|
|
|
21
29
|
_SCHEMA = """
|
|
@@ -51,6 +59,18 @@ CREATE TABLE input_measurement (
|
|
|
51
59
|
CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
|
|
52
60
|
CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
|
|
53
61
|
CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
|
|
62
|
+
CREATE TABLE assay_pair (
|
|
63
|
+
reference_key TEXT NOT NULL REFERENCES reference(key), id TEXT NOT NULL,
|
|
64
|
+
left_assay_id TEXT, right_assay_id TEXT, record TEXT NOT NULL,
|
|
65
|
+
PRIMARY KEY(reference_key, id),
|
|
66
|
+
FOREIGN KEY(reference_key, left_assay_id) REFERENCES assay(reference_key, id),
|
|
67
|
+
FOREIGN KEY(reference_key, right_assay_id) REFERENCES assay(reference_key, id));
|
|
68
|
+
CREATE TABLE target_relation (
|
|
69
|
+
reference_key TEXT NOT NULL, id TEXT NOT NULL,
|
|
70
|
+
left_target_key TEXT NOT NULL, right_target_key TEXT NOT NULL, record TEXT NOT NULL,
|
|
71
|
+
PRIMARY KEY(reference_key, id),
|
|
72
|
+
FOREIGN KEY(reference_key, left_target_key) REFERENCES target(reference_key, key),
|
|
73
|
+
FOREIGN KEY(reference_key, right_target_key) REFERENCES target(reference_key, key));
|
|
54
74
|
"""
|
|
55
75
|
|
|
56
76
|
|
|
@@ -73,7 +93,8 @@ class QuantitativeDataset:
|
|
|
73
93
|
def __init__(self, path):
|
|
74
94
|
self.path = Path(path).resolve()
|
|
75
95
|
with closing(self._connect()) as connection:
|
|
76
|
-
|
|
96
|
+
self._storage_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
|
97
|
+
if self._storage_version not in (1, 2):
|
|
77
98
|
raise ValueError("Unsupported quantitative database schema")
|
|
78
99
|
row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
|
|
79
100
|
if row is None:
|
|
@@ -88,7 +109,8 @@ class QuantitativeDataset:
|
|
|
88
109
|
return connection
|
|
89
110
|
|
|
90
111
|
@classmethod
|
|
91
|
-
def create(cls, path, manifest, *, samples, assays, measurements, mappings=()
|
|
112
|
+
def create(cls, path, manifest, *, samples, assays, measurements, mappings=(),
|
|
113
|
+
pairings=(), relations=()):
|
|
92
114
|
"""Write a new dataset from iterators, validating scope and foreign keys.
|
|
93
115
|
|
|
94
116
|
Never overwrite an existing file. An interrupted/invalid build removes
|
|
@@ -106,7 +128,7 @@ class QuantitativeDataset:
|
|
|
106
128
|
connection = sqlite3.connect(path)
|
|
107
129
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
108
130
|
connection.executescript(_SCHEMA)
|
|
109
|
-
connection.execute("PRAGMA user_version=
|
|
131
|
+
connection.execute("PRAGMA user_version=2")
|
|
110
132
|
connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
|
|
111
133
|
(_json(manifest.to_dict()),))
|
|
112
134
|
for reference in manifest.references:
|
|
@@ -125,6 +147,45 @@ class QuantitativeDataset:
|
|
|
125
147
|
connection.execute("INSERT INTO assay VALUES (?, ?, ?, ?)",
|
|
126
148
|
(assay.reference.key, assay.assay_id, assay.sample_id,
|
|
127
149
|
_json(assay.to_dict())))
|
|
150
|
+
for pair in pairings:
|
|
151
|
+
if not isinstance(pair, AssayPair):
|
|
152
|
+
raise TypeError("pairings must contain AssayPairs")
|
|
153
|
+
if (pair.dataset_id, pair.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
154
|
+
raise ValueError("Pairing belongs to a different dataset release")
|
|
155
|
+
if pair.basis in ("same_material", "adjacent_donor_tissue"):
|
|
156
|
+
units = []
|
|
157
|
+
for identifier in (pair.left_assay_id, pair.right_assay_id):
|
|
158
|
+
row = connection.execute(
|
|
159
|
+
"SELECT s.record FROM assay a JOIN sample s ON a.reference_key=s.reference_key "
|
|
160
|
+
"AND a.sample_id=s.id WHERE a.reference_key=? AND a.id=?",
|
|
161
|
+
(pair.reference.key, identifier)).fetchone()
|
|
162
|
+
if row:
|
|
163
|
+
unit = json.loads(row[0]).get("biological_unit_id")
|
|
164
|
+
if unit is not None:
|
|
165
|
+
units.append(unit)
|
|
166
|
+
if len(set(units)) > 1:
|
|
167
|
+
raise ValueError("Declared pairing contradicts biological-unit identities")
|
|
168
|
+
connection.execute("INSERT INTO assay_pair VALUES (?, ?, ?, ?, ?)",
|
|
169
|
+
(pair.reference.key, pair.pair_id, pair.left_assay_id,
|
|
170
|
+
pair.right_assay_id, _json(pair.to_dict())))
|
|
171
|
+
relation_links = ({}, {})
|
|
172
|
+
for relation in relations:
|
|
173
|
+
if not isinstance(relation, TargetRelation):
|
|
174
|
+
raise TypeError("relations must contain TargetRelations")
|
|
175
|
+
if (relation.dataset_id, relation.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
176
|
+
raise ValueError("Target relation belongs to a different dataset release")
|
|
177
|
+
for target in (relation.left, relation.right):
|
|
178
|
+
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
179
|
+
(target.reference.key, _target_key(target), target.kind,
|
|
180
|
+
target.namespace, target.identifier))
|
|
181
|
+
for index, (first, second) in enumerate(((relation.left, relation.right), (relation.right, relation.left))):
|
|
182
|
+
_check_one_to_one(relation_links[index],
|
|
183
|
+
(relation.relation, first.key, second.kind, second.namespace), second.key,
|
|
184
|
+
relation.cardinality == "one_to_one")
|
|
185
|
+
connection.execute("INSERT INTO target_relation VALUES (?, ?, ?, ?, ?)",
|
|
186
|
+
(relation.left.reference.key, relation.relation_id,
|
|
187
|
+
_target_key(relation.left), _target_key(relation.right),
|
|
188
|
+
_json(relation.to_dict())))
|
|
128
189
|
for mapping in mappings:
|
|
129
190
|
if not isinstance(mapping, TargetMapping):
|
|
130
191
|
raise TypeError("mappings must contain TargetMappings")
|
|
@@ -164,7 +225,8 @@ class QuantitativeDataset:
|
|
|
164
225
|
((reference_key, observation.measurement_id, identifier)
|
|
165
226
|
for identifier in observation.input_measurements))
|
|
166
227
|
counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
|
|
167
|
-
for table in ("sample", "assay", "mapping", "target", "measurement"
|
|
228
|
+
for table in ("sample", "assay", "mapping", "target", "measurement",
|
|
229
|
+
"assay_pair", "target_relation")}
|
|
168
230
|
connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
|
|
169
231
|
connection.commit()
|
|
170
232
|
connection.close()
|
|
@@ -205,6 +267,58 @@ class QuantitativeDataset:
|
|
|
205
267
|
def mappings(self, *, reference=None):
|
|
206
268
|
return self._records("mapping", TargetMapping, reference=reference)
|
|
207
269
|
|
|
270
|
+
def pairings(self, *, reference=None):
|
|
271
|
+
"""Iterate source-declared pairings; legacy releases have none."""
|
|
272
|
+
if self._storage_version == 1:
|
|
273
|
+
self._scope(reference)
|
|
274
|
+
return iter(())
|
|
275
|
+
return self._records("assay_pair", AssayPair, reference=reference)
|
|
276
|
+
|
|
277
|
+
def relations(self, *, reference=None):
|
|
278
|
+
"""Iterate source-backed within-reference target relationships."""
|
|
279
|
+
if self._storage_version == 1:
|
|
280
|
+
self._scope(reference)
|
|
281
|
+
return iter(())
|
|
282
|
+
return self._records("target_relation", TargetRelation, reference=reference)
|
|
283
|
+
|
|
284
|
+
def matched_measurements(self, *, allowed_bases, allowed_temporal=("concurrent",),
|
|
285
|
+
reference=None, pair_ids=None, relation_ids=None):
|
|
286
|
+
"""Iterate explicit assay/target matches, preserving all values and missing mates.
|
|
287
|
+
|
|
288
|
+
Defaults require concurrent assays; opting into longitudinal or unknown
|
|
289
|
+
timing is explicit. No matching by tissue names or inferred orthology.
|
|
290
|
+
Each side contains its own original observations, never a Cartesian
|
|
291
|
+
multiplication or an aggregate over technical replicates/quantities.
|
|
292
|
+
"""
|
|
293
|
+
bases, temporal = set(allowed_bases), set(allowed_temporal)
|
|
294
|
+
if isinstance(pair_ids, str) or isinstance(relation_ids, str):
|
|
295
|
+
raise TypeError("Relationship ID selections must be sequences, not a single string")
|
|
296
|
+
if not bases or bases - set(PAIRING_BASES):
|
|
297
|
+
raise ValueError("Specify valid allowed pairing bases explicitly")
|
|
298
|
+
if not temporal or temporal - set(TEMPORAL_RELATIONS):
|
|
299
|
+
raise ValueError("Specify valid allowed temporal relationships")
|
|
300
|
+
if (pair_ids or relation_ids) and reference is None and len(self._references) > 1:
|
|
301
|
+
raise ValueError("Relationship ID queries across species require a reference")
|
|
302
|
+
relations = tuple(relation for relation in self.relations(reference=reference)
|
|
303
|
+
if relation_ids is None or relation.relation_id in relation_ids)
|
|
304
|
+
for pair in self.pairings(reference=reference):
|
|
305
|
+
if pair.basis not in bases or pair.temporal_relation not in temporal:
|
|
306
|
+
continue
|
|
307
|
+
if pair_ids is not None and pair.pair_id not in pair_ids:
|
|
308
|
+
continue
|
|
309
|
+
by_target = []
|
|
310
|
+
for assay in (pair.left_assay_id, pair.right_assay_id):
|
|
311
|
+
values = {}
|
|
312
|
+
if assay is not None:
|
|
313
|
+
for value in self.measurements(reference=pair.reference, assay_id=assay):
|
|
314
|
+
values.setdefault(value.target.key, []).append(value)
|
|
315
|
+
by_target.append(values)
|
|
316
|
+
for relation in relations:
|
|
317
|
+
if relation.left.reference.key == pair.reference.key:
|
|
318
|
+
yield MatchedMeasurements(pair, relation,
|
|
319
|
+
tuple(by_target[0].get(relation.left.key, ())),
|
|
320
|
+
tuple(by_target[1].get(relation.right.key, ())))
|
|
321
|
+
|
|
208
322
|
def measurements(self, *, target=None, reference=None, sample_id=None,
|
|
209
323
|
assay_id=None, quantity=None, status=None):
|
|
210
324
|
"""Iterate native observations; a target match is exact, never orthology.
|
|
@@ -18,7 +18,7 @@ def run(argv=None, prog=None):
|
|
|
18
18
|
listing.add_argument("--species")
|
|
19
19
|
listing.add_argument("--taxon-id", type=int)
|
|
20
20
|
listing.add_argument("--assay")
|
|
21
|
-
for command in ("info", "download", "inspect", "query"):
|
|
21
|
+
for command in ("info", "download", "inspect", "query", "pairs"):
|
|
22
22
|
subparser = commands.add_parser(command)
|
|
23
23
|
subparser.add_argument("name")
|
|
24
24
|
subparser.add_argument("--version")
|
|
@@ -36,6 +36,14 @@ def run(argv=None, prog=None):
|
|
|
36
36
|
subparser.add_argument("--target")
|
|
37
37
|
subparser.add_argument("--kind")
|
|
38
38
|
subparser.add_argument("--namespace")
|
|
39
|
+
if command == "pairs":
|
|
40
|
+
subparser.add_argument("--basis", action="append", required=True,
|
|
41
|
+
help="Allowed pairing basis; repeat for multiple bases")
|
|
42
|
+
subparser.add_argument("--temporal", action="append",
|
|
43
|
+
help="Allowed temporal relation; default concurrent")
|
|
44
|
+
subparser.add_argument("--reference")
|
|
45
|
+
subparser.add_argument("--pair-id", action="append")
|
|
46
|
+
subparser.add_argument("--relation-id", action="append")
|
|
39
47
|
args = parser.parse_args(argv)
|
|
40
48
|
try:
|
|
41
49
|
definitions = None
|
|
@@ -60,6 +68,14 @@ def run(argv=None, prog=None):
|
|
|
60
68
|
sources = {"dataset.sqlite": args.source_path} if args.source_path else None
|
|
61
69
|
result = registry.download(args.name, args.version, force=args.force,
|
|
62
70
|
source_paths=sources, **options)
|
|
71
|
+
elif args.command == "pairs":
|
|
72
|
+
dataset = registry.open(args.name, args.version)
|
|
73
|
+
for match in dataset.matched_measurements(
|
|
74
|
+
allowed_bases=args.basis, allowed_temporal=args.temporal or ["concurrent"],
|
|
75
|
+
reference=args.reference, pair_ids=args.pair_id, relation_ids=args.relation_id,
|
|
76
|
+
):
|
|
77
|
+
sys.stdout.write(json.dumps(match.to_table_row()) + "\n")
|
|
78
|
+
return
|
|
63
79
|
else:
|
|
64
80
|
dataset = registry.open(args.name, args.version)
|
|
65
81
|
target = None
|
|
@@ -13,6 +13,7 @@ ASSAY_KINDS = ("rna_seq", "ribo_seq", "digest_ms", "mhc_ms", "targeted_ms")
|
|
|
13
13
|
QUANTITIES = (
|
|
14
14
|
"rna_abundance", "ribosome_occupancy", "translation_efficiency",
|
|
15
15
|
"protein_abundance", "peptide_abundance", "spectrum_count", "turnover",
|
|
16
|
+
"protein_rna_ratio", "abundance_ratio",
|
|
16
17
|
)
|
|
17
18
|
MEASUREMENT_STATUSES = ("measured", "below_detection", "not_measured", "not_mapped", "missing")
|
|
18
19
|
MEASUREMENT_ORIGINS = ("observed", "derived", "predicted")
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Explicit assay relationships and comparisons of original observations."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .measurements import Measurement, SourceAsset, _identifier, _metadata, _number, _Record
|
|
8
|
+
from .reference import ReferenceContext, ScopedTarget
|
|
9
|
+
|
|
10
|
+
PAIRING_BASES = (
|
|
11
|
+
"same_material", "adjacent_donor_tissue", "biological_replicate",
|
|
12
|
+
"independent_cultures", "tissue_aggregate", "condition_aggregate",
|
|
13
|
+
)
|
|
14
|
+
TEMPORAL_RELATIONS = ("concurrent", "longitudinal", "unknown")
|
|
15
|
+
TARGET_RELATIONS = ("same_target", "canonical_translation", "parent_rna_context", "shared_group_member")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _target(data):
|
|
19
|
+
values = dict(data)
|
|
20
|
+
values["reference"] = ReferenceContext(**values["reference"])
|
|
21
|
+
return ScopedTarget(**values)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _release(record):
|
|
25
|
+
for name in ("dataset_id", "dataset_version"):
|
|
26
|
+
_identifier(getattr(record, name), name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_one_to_one(seen, source_key, target_key, one_to_one):
|
|
30
|
+
"""Check a claimed one-to-one edge against every supplied candidate edge."""
|
|
31
|
+
targets, strict = seen.setdefault(source_key, (set(), False))
|
|
32
|
+
targets.add(target_key)
|
|
33
|
+
strict = strict or one_to_one
|
|
34
|
+
if strict and len(targets) > 1:
|
|
35
|
+
raise ValueError("Conflicting one-to-one target relationships")
|
|
36
|
+
seen[source_key] = (targets, strict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class AssayPair(_Record):
|
|
41
|
+
"""A source-backed assay pairing, including an explicitly missing mate.
|
|
42
|
+
|
|
43
|
+
The pairing basis and temporal relationship are independent. A shared tissue
|
|
44
|
+
label supplies neither. Replicate/aliquot identity remains in the original
|
|
45
|
+
samples and assays, rather than being counted from the number of pair rows.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
pair_id: str
|
|
49
|
+
reference: ReferenceContext
|
|
50
|
+
dataset_id: str
|
|
51
|
+
dataset_version: str
|
|
52
|
+
left_assay_id: Optional[str]
|
|
53
|
+
right_assay_id: Optional[str]
|
|
54
|
+
basis: str
|
|
55
|
+
evidence: Mapping
|
|
56
|
+
temporal_relation: str = "unknown"
|
|
57
|
+
interval: Optional[float] = None
|
|
58
|
+
interval_unit: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def __post_init__(self):
|
|
61
|
+
_identifier(self.pair_id, "pair_id")
|
|
62
|
+
_release(self)
|
|
63
|
+
if not isinstance(self.reference, ReferenceContext):
|
|
64
|
+
raise TypeError("Pair requires a ReferenceContext")
|
|
65
|
+
for identifier in (self.left_assay_id, self.right_assay_id):
|
|
66
|
+
if identifier is not None:
|
|
67
|
+
_identifier(identifier, "assay ID")
|
|
68
|
+
if self.left_assay_id is None and self.right_assay_id is None:
|
|
69
|
+
raise ValueError("A pairing requires at least one declared assay")
|
|
70
|
+
if self.left_assay_id == self.right_assay_id:
|
|
71
|
+
raise ValueError("A pairing requires distinct assays")
|
|
72
|
+
if self.basis not in PAIRING_BASES:
|
|
73
|
+
raise ValueError("Unknown assay pairing basis")
|
|
74
|
+
if self.temporal_relation not in TEMPORAL_RELATIONS:
|
|
75
|
+
raise ValueError("Unknown temporal relationship")
|
|
76
|
+
if not self.evidence:
|
|
77
|
+
raise ValueError("Pairing requires source evidence")
|
|
78
|
+
if self.interval is not None:
|
|
79
|
+
_number(self.interval, "interval")
|
|
80
|
+
_identifier(self.interval_unit, "interval_unit")
|
|
81
|
+
if self.temporal_relation == "concurrent" and self.interval != 0:
|
|
82
|
+
raise ValueError("Concurrent assays cannot have a nonzero interval")
|
|
83
|
+
elif self.interval_unit is not None:
|
|
84
|
+
raise ValueError("Interval units require a value")
|
|
85
|
+
_metadata(self, "evidence")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class TargetRelation(_Record):
|
|
90
|
+
"""Explain why two exact targets can be compared without changing either ID."""
|
|
91
|
+
|
|
92
|
+
relation_id: str
|
|
93
|
+
dataset_id: str
|
|
94
|
+
dataset_version: str
|
|
95
|
+
left: ScopedTarget
|
|
96
|
+
right: ScopedTarget
|
|
97
|
+
relation: str
|
|
98
|
+
cardinality: str
|
|
99
|
+
evidence: Mapping
|
|
100
|
+
|
|
101
|
+
def __post_init__(self):
|
|
102
|
+
_identifier(self.relation_id, "relation_id")
|
|
103
|
+
_release(self)
|
|
104
|
+
if not isinstance(self.left, ScopedTarget) or not isinstance(self.right, ScopedTarget):
|
|
105
|
+
raise TypeError("Target relations require ScopedTargets")
|
|
106
|
+
if self.left.reference.key != self.right.reference.key:
|
|
107
|
+
raise ValueError("Within-dataset target relations cannot cross reference contexts")
|
|
108
|
+
if self.relation not in TARGET_RELATIONS:
|
|
109
|
+
raise ValueError("Unknown target relationship")
|
|
110
|
+
if self.cardinality not in ("one_to_one", "one_to_many", "many_to_one", "many_to_many", "unknown"):
|
|
111
|
+
raise ValueError("Unknown target relationship cardinality")
|
|
112
|
+
if not self.evidence:
|
|
113
|
+
raise ValueError("Target relations require mapping evidence")
|
|
114
|
+
kinds = (self.left.kind, self.right.kind)
|
|
115
|
+
if self.relation == "same_target" and self.left.key != self.right.key:
|
|
116
|
+
raise ValueError("same_target requires identical scoped target identities")
|
|
117
|
+
if self.relation == "same_target" and self.cardinality != "one_to_one":
|
|
118
|
+
raise ValueError("An identical target has one-to-one cardinality")
|
|
119
|
+
if self.relation == "canonical_translation" and kinds not in (("gene", "protein"), ("transcript", "protein")):
|
|
120
|
+
raise ValueError("Canonical translation links a gene/transcript to a protein")
|
|
121
|
+
if self.relation == "parent_rna_context" and kinds not in (("gene", "orf"), ("transcript", "orf")):
|
|
122
|
+
raise ValueError("Parent RNA context links a gene/transcript to an ORF")
|
|
123
|
+
if self.relation == "shared_group_member" and kinds not in (("protein_group", "protein"), ("protein_group", "orf")):
|
|
124
|
+
raise ValueError("Group membership links a group to one candidate member")
|
|
125
|
+
_metadata(self, "evidence")
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_dict(cls, data):
|
|
129
|
+
values = dict(data)
|
|
130
|
+
values["left"], values["right"] = _target(values["left"]), _target(values["right"])
|
|
131
|
+
return cls(**values)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class MatchedMeasurements(_Record):
|
|
136
|
+
"""One declared relationship with all original values and explicit missingness.
|
|
137
|
+
|
|
138
|
+
Tuples avoid multiplying technical/quantity records into pseudo-replicates.
|
|
139
|
+
For parent_rna_context, left values remain measurements of the parent RNA.
|
|
140
|
+
They are never measurements of the ORF named on the right.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
pairing: AssayPair
|
|
144
|
+
relation: TargetRelation
|
|
145
|
+
left: tuple
|
|
146
|
+
right: tuple
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dict(cls, data):
|
|
150
|
+
return cls(AssayPair.from_dict(data["pairing"]), TargetRelation.from_dict(data["relation"]),
|
|
151
|
+
tuple(Measurement.from_dict(value) for value in data["left"]),
|
|
152
|
+
tuple(Measurement.from_dict(value) for value in data["right"]))
|
|
153
|
+
|
|
154
|
+
def __post_init__(self):
|
|
155
|
+
pair, relation = self.pairing, self.relation
|
|
156
|
+
if not isinstance(pair, AssayPair) or not isinstance(relation, TargetRelation):
|
|
157
|
+
raise TypeError("A match requires an AssayPair and TargetRelation")
|
|
158
|
+
if (pair.dataset_id, pair.dataset_version) != (relation.dataset_id, relation.dataset_version):
|
|
159
|
+
raise ValueError("Pairing and target relation belong to different dataset releases")
|
|
160
|
+
if pair.reference.key != relation.left.reference.key:
|
|
161
|
+
raise ValueError("Pairing and target relation have different references")
|
|
162
|
+
for side in ("left", "right"):
|
|
163
|
+
values = tuple(getattr(self, side))
|
|
164
|
+
target = getattr(relation, side)
|
|
165
|
+
assay_id = getattr(pair, side + "_assay_id")
|
|
166
|
+
seen = set()
|
|
167
|
+
for value in values:
|
|
168
|
+
if not isinstance(value, Measurement):
|
|
169
|
+
raise TypeError("Matched values must be Measurements")
|
|
170
|
+
if (value.dataset_id, value.dataset_version) != (pair.dataset_id, pair.dataset_version):
|
|
171
|
+
raise ValueError("Matched measurement belongs to a different dataset release")
|
|
172
|
+
if value.target.key != target.key or value.assay_id != assay_id:
|
|
173
|
+
raise ValueError("Matched observation disagrees with its declared target/assay")
|
|
174
|
+
if value.measurement_id in seen:
|
|
175
|
+
raise ValueError("Duplicate matched measurement")
|
|
176
|
+
seen.add(value.measurement_id)
|
|
177
|
+
object.__setattr__(self, side, values)
|
|
178
|
+
|
|
179
|
+
def to_table_row(self):
|
|
180
|
+
"""Return named assay-quantity columns with original units and provenance.
|
|
181
|
+
|
|
182
|
+
Columns contain lists, preserving multiple measurements without silently
|
|
183
|
+
averaging them. Empty columns are missing observations, not measured zero.
|
|
184
|
+
Pairing and target relationships remain present to qualify comparisons.
|
|
185
|
+
"""
|
|
186
|
+
columns = {name: [] for name in (
|
|
187
|
+
"rna_abundance", "ribosome_occupancy", "translation_efficiency",
|
|
188
|
+
"protein_abundance", "peptide_abundance", "spectrum_count", "other")}
|
|
189
|
+
for value in self.left + self.right:
|
|
190
|
+
key = value.quantity if value.quantity in columns else "other"
|
|
191
|
+
columns[key].append(value.to_dict())
|
|
192
|
+
return dict(dataset_id=self.pairing.dataset_id, dataset_version=self.pairing.dataset_version,
|
|
193
|
+
pairing=self.pairing.to_dict(), target_relation=self.relation.to_dict(), **columns)
|
|
194
|
+
|
|
195
|
+
def ratio(self, numerator, denominator, *, measurement_id, method, normalization):
|
|
196
|
+
"""Derive a named native-unit ratio from explicitly selected observations.
|
|
197
|
+
|
|
198
|
+
No implicit log conversion, replicate aggregation, unit conversion or
|
|
199
|
+
uncertainty propagation. Undefined ratios are not_measured with a reason.
|
|
200
|
+
Parent RNA and ambiguous group membership cannot produce ORF ratios.
|
|
201
|
+
"""
|
|
202
|
+
_identifier(method, "ratio method")
|
|
203
|
+
if not isinstance(normalization, Mapping) or not normalization:
|
|
204
|
+
raise ValueError("Ratio normalization and assumptions must be explicit")
|
|
205
|
+
if self.relation.relation not in ("same_target", "canonical_translation"):
|
|
206
|
+
raise ValueError("Parent RNA/group membership cannot establish target-specific ratios")
|
|
207
|
+
if self.relation.cardinality != "one_to_one":
|
|
208
|
+
raise ValueError("A target-specific ratio requires one-to-one attribution")
|
|
209
|
+
if not ((numerator in self.left and denominator in self.right)
|
|
210
|
+
or (numerator in self.right and denominator in self.left)):
|
|
211
|
+
raise ValueError("Ratio inputs must be original measurements on opposite sides")
|
|
212
|
+
if numerator.scale != "linear" or denominator.scale != "linear":
|
|
213
|
+
raise ValueError("Ratio inputs must be linear; any log conversion must be explicit")
|
|
214
|
+
if (numerator.quantity, denominator.quantity) == ("protein_abundance", "rna_abundance"):
|
|
215
|
+
quantity = "protein_rna_ratio"
|
|
216
|
+
elif (numerator.quantity, denominator.quantity) == ("ribosome_occupancy", "rna_abundance"):
|
|
217
|
+
quantity = "translation_efficiency"
|
|
218
|
+
elif numerator.quantity == denominator.quantity:
|
|
219
|
+
quantity = "abundance_ratio"
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError("Unsupported ratio quantity relationship")
|
|
222
|
+
reason = None
|
|
223
|
+
if numerator.status != "measured" or denominator.status != "measured":
|
|
224
|
+
reason = "input_not_measured"
|
|
225
|
+
elif denominator.value <= 0:
|
|
226
|
+
reason = "nonpositive_denominator"
|
|
227
|
+
derivation = dict(method=method, normalization=dict(normalization),
|
|
228
|
+
pairing=self.pairing.to_dict(), target_relation=self.relation.to_dict(),
|
|
229
|
+
uncertainty="not_propagated", unresolved_reason=reason)
|
|
230
|
+
return Measurement(
|
|
231
|
+
measurement_id=measurement_id, target=numerator.target,
|
|
232
|
+
sample_id=numerator.sample_id, assay_id=numerator.assay_id, quantity=quantity,
|
|
233
|
+
value=None if reason else numerator.value / denominator.value,
|
|
234
|
+
unit=f"({numerator.unit})/({denominator.unit})",
|
|
235
|
+
dataset_id=numerator.dataset_id, dataset_version=numerator.dataset_version,
|
|
236
|
+
status="not_measured" if reason else "measured", origin="derived",
|
|
237
|
+
input_measurements=(numerator.measurement_id, denominator.measurement_id),
|
|
238
|
+
derivation=derivation,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass(frozen=True)
|
|
243
|
+
class OrthologyMapping(_Record):
|
|
244
|
+
"""Pinned cross-species mapping; it never transfers measurements or evidence."""
|
|
245
|
+
|
|
246
|
+
mapping_id: str
|
|
247
|
+
left: ScopedTarget
|
|
248
|
+
right: ScopedTarget
|
|
249
|
+
source: SourceAsset
|
|
250
|
+
source_release: str
|
|
251
|
+
cardinality: str
|
|
252
|
+
confidence: Mapping
|
|
253
|
+
evidence: Mapping
|
|
254
|
+
|
|
255
|
+
def __post_init__(self):
|
|
256
|
+
_identifier(self.mapping_id, "mapping_id")
|
|
257
|
+
_identifier(self.source_release, "source_release")
|
|
258
|
+
if not isinstance(self.left, ScopedTarget) or not isinstance(self.right, ScopedTarget):
|
|
259
|
+
raise TypeError("Orthology requires ScopedTargets")
|
|
260
|
+
if self.left.reference.taxon_id == self.right.reference.taxon_id:
|
|
261
|
+
raise ValueError("Orthology comparison requires different species")
|
|
262
|
+
if not isinstance(self.source, SourceAsset):
|
|
263
|
+
raise TypeError("Orthology source must have a pinned SourceAsset")
|
|
264
|
+
if self.cardinality not in ("one_to_one", "one_to_many", "many_to_one", "many_to_many"):
|
|
265
|
+
raise ValueError("Unknown orthology cardinality")
|
|
266
|
+
if (self.left.kind, self.right.kind) not in (("gene", "gene"), ("orf", "orf")):
|
|
267
|
+
raise ValueError("Orthology maps canonical genes or independently supported ORFs")
|
|
268
|
+
if not self.evidence or not self.confidence:
|
|
269
|
+
raise ValueError("Orthology evidence and source confidence definition are required")
|
|
270
|
+
if self.left.kind == "orf" and not all(self.evidence.get(key) for key in
|
|
271
|
+
("sequence", "frame", "position")):
|
|
272
|
+
raise ValueError("ORF orthology requires its own sequence, frame and position evidence")
|
|
273
|
+
_metadata(self, "confidence", "evidence")
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_dict(cls, data):
|
|
277
|
+
values = dict(data)
|
|
278
|
+
values["left"], values["right"] = _target(values["left"]), _target(values["right"])
|
|
279
|
+
values["source"] = SourceAsset.from_dict(values["source"])
|
|
280
|
+
return cls(**values)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass(frozen=True)
|
|
284
|
+
class OrthologyComparison(_Record):
|
|
285
|
+
mapping: OrthologyMapping
|
|
286
|
+
left: tuple
|
|
287
|
+
right: tuple
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def from_dict(cls, data):
|
|
291
|
+
return cls(OrthologyMapping.from_dict(data["mapping"]),
|
|
292
|
+
tuple(Measurement.from_dict(value) for value in data["left"]),
|
|
293
|
+
tuple(Measurement.from_dict(value) for value in data["right"]))
|
|
294
|
+
|
|
295
|
+
def __post_init__(self):
|
|
296
|
+
if not isinstance(self.mapping, OrthologyMapping):
|
|
297
|
+
raise TypeError("Comparison requires an OrthologyMapping")
|
|
298
|
+
for side in ("left", "right"):
|
|
299
|
+
values = tuple(getattr(self, side))
|
|
300
|
+
target = getattr(self.mapping, side)
|
|
301
|
+
if any(not isinstance(value, Measurement) or value.target.key != target.key for value in values):
|
|
302
|
+
raise ValueError("Orthology observations must retain their original scoped targets")
|
|
303
|
+
object.__setattr__(self, side, values)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def compare_orthologs(left_dataset, right_dataset, mappings, *,
|
|
307
|
+
allowed_cardinalities=("one_to_one",), allow_orfs=False,
|
|
308
|
+
left_filters=None, right_filters=None):
|
|
309
|
+
"""Return both species' original observations for explicitly opted-in mappings."""
|
|
310
|
+
allowed = set(allowed_cardinalities)
|
|
311
|
+
valid = {"one_to_one", "one_to_many", "many_to_one", "many_to_many"}
|
|
312
|
+
if not allowed or allowed - valid:
|
|
313
|
+
raise ValueError("Explicit valid orthology cardinalities are required")
|
|
314
|
+
mappings = tuple(mappings)
|
|
315
|
+
seen = ({}, {})
|
|
316
|
+
for mapping in mappings:
|
|
317
|
+
if not isinstance(mapping, OrthologyMapping):
|
|
318
|
+
raise TypeError("Expected OrthologyMapping records")
|
|
319
|
+
for index, (source, target) in enumerate(((mapping.left, mapping.right), (mapping.right, mapping.left))):
|
|
320
|
+
_check_one_to_one(seen[index], (source.key, target.reference.key, target.namespace), target.key,
|
|
321
|
+
mapping.cardinality == "one_to_one")
|
|
322
|
+
for mapping in mappings:
|
|
323
|
+
if mapping.cardinality not in allowed or (mapping.left.kind == "orf" and not allow_orfs):
|
|
324
|
+
continue
|
|
325
|
+
yield OrthologyComparison(
|
|
326
|
+
mapping,
|
|
327
|
+
tuple(left_dataset.measurements(target=mapping.left, **(left_filters or {}))),
|
|
328
|
+
tuple(right_dataset.measurements(target=mapping.right, **(right_filters or {}))),
|
|
329
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -7,6 +7,7 @@ pyproject.toml
|
|
|
7
7
|
test.sh
|
|
8
8
|
docs/design.md
|
|
9
9
|
docs/measurements.md
|
|
10
|
+
docs/pairing.md
|
|
10
11
|
docs/reference.md
|
|
11
12
|
docs/species.md
|
|
12
13
|
orfdata/__init__.py
|
|
@@ -19,6 +20,7 @@ orfdata/datasets.py
|
|
|
19
20
|
orfdata/initiation_context.py
|
|
20
21
|
orfdata/measurements.py
|
|
21
22
|
orfdata/model.py
|
|
23
|
+
orfdata/pairing.py
|
|
22
24
|
orfdata/reference.py
|
|
23
25
|
orfdata/sequence.py
|
|
24
26
|
orfdata/version.py
|
|
@@ -37,8 +39,10 @@ orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt
|
|
|
37
39
|
orfdata/data/uorf-evidence/NOTICE.txt
|
|
38
40
|
orfdata/data/uorf-evidence/catalog.json.gz
|
|
39
41
|
orfdata/data/uorf-evidence/manifest.json
|
|
42
|
+
tests/conftest.py
|
|
40
43
|
tests/test_initiation_context.py
|
|
41
44
|
tests/test_measurements.py
|
|
45
|
+
tests/test_pairing.py
|
|
42
46
|
tests/test_reference_context.py
|
|
43
47
|
tests/test_reference_orf.py
|
|
44
48
|
tests/test_uorf_catalog.py
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Shared synthetic quantitative fixtures; these are not experimental evidence."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from orfdata import (
|
|
8
|
+
Assay,
|
|
9
|
+
DatasetManifest,
|
|
10
|
+
Measurement,
|
|
11
|
+
ReferenceContext,
|
|
12
|
+
Sample,
|
|
13
|
+
ScopedTarget,
|
|
14
|
+
SourceAsset,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def experiment(tmp_path):
|
|
20
|
+
source = tmp_path / "input.tsv"
|
|
21
|
+
source.write_text("source fixture\n")
|
|
22
|
+
contexts = tuple(ReferenceContext(taxon, species, assembly, "ensembl", "110")
|
|
23
|
+
for taxon, species, assembly in (
|
|
24
|
+
(9606, "human", "GRCh38"), (10090, "mouse", "GRCm39"),
|
|
25
|
+
(9615, "dog", "ROS_Cfam_1.0"), (4932, "yeast", "R64-1-1")))
|
|
26
|
+
manifest = DatasetManifest(
|
|
27
|
+
"fixture", "1", "Synthetic four-species measurement validation", contexts,
|
|
28
|
+
(SourceAsset("input.tsv", source.as_uri(), hashlib.sha256(source.read_bytes()).hexdigest(),
|
|
29
|
+
source.stat().st_size, "Synthetic test data; Apache-2.0", redistributable=True),),
|
|
30
|
+
("Synthetic test fixture, not experimental evidence",), ("rna_seq", "digest_ms", "ribo_seq"),
|
|
31
|
+
dict(searched="Deliberately colliding test identifiers", exclusions="No biological inference"),
|
|
32
|
+
dict(software="orfdata tests", version="1", parameters={} ),
|
|
33
|
+
)
|
|
34
|
+
samples = tuple(Sample("sample", context, biological_unit_id="animal",
|
|
35
|
+
attributes={"source_taxon": context.taxon_id}) for context in contexts)
|
|
36
|
+
assays = tuple(Assay("rna", "sample", "rna_seq", context, technical_replicate="1")
|
|
37
|
+
for context in contexts)
|
|
38
|
+
observations = tuple(Measurement(
|
|
39
|
+
"value", ScopedTarget(context, "gene", "same_id", "ensembl:110"),
|
|
40
|
+
"sample", "rna", "rna_abundance", index, "TPM", "fixture", "1",
|
|
41
|
+
provenance=dict(source_asset="input.tsv", row=index + 1),
|
|
42
|
+
) for index, context in enumerate(contexts))
|
|
43
|
+
return manifest, samples, assays, observations
|
|
44
|
+
|
|
45
|
+
|
|
@@ -9,15 +9,11 @@ from types import SimpleNamespace
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
11
|
from orfdata import (
|
|
12
|
-
Assay,
|
|
13
12
|
DatasetManifest,
|
|
14
13
|
DatasetRegistry,
|
|
15
14
|
Measurement,
|
|
16
15
|
QuantitativeDataset,
|
|
17
|
-
ReferenceContext,
|
|
18
|
-
Sample,
|
|
19
16
|
ScopedTarget,
|
|
20
|
-
SourceAsset,
|
|
21
17
|
TargetMapping,
|
|
22
18
|
build_dataset,
|
|
23
19
|
)
|
|
@@ -25,34 +21,6 @@ from orfdata.cli import run
|
|
|
25
21
|
from orfdata.measurements import MEASUREMENT_STATUSES
|
|
26
22
|
|
|
27
23
|
|
|
28
|
-
@pytest.fixture
|
|
29
|
-
def experiment(tmp_path):
|
|
30
|
-
source = tmp_path / "input.tsv"
|
|
31
|
-
source.write_text("source fixture\n")
|
|
32
|
-
contexts = tuple(ReferenceContext(taxon, species, assembly, "ensembl", "110")
|
|
33
|
-
for taxon, species, assembly in (
|
|
34
|
-
(9606, "human", "GRCh38"), (10090, "mouse", "GRCm39"),
|
|
35
|
-
(9615, "dog", "ROS_Cfam_1.0"), (4932, "yeast", "R64-1-1")))
|
|
36
|
-
manifest = DatasetManifest(
|
|
37
|
-
"fixture", "1", "Synthetic four-species measurement validation", contexts,
|
|
38
|
-
(SourceAsset("input.tsv", source.as_uri(), hashlib.sha256(source.read_bytes()).hexdigest(),
|
|
39
|
-
source.stat().st_size, "Synthetic test data; Apache-2.0", redistributable=True),),
|
|
40
|
-
("Synthetic test fixture, not experimental evidence",), ("rna_seq", "digest_ms", "ribo_seq"),
|
|
41
|
-
dict(searched="Deliberately colliding test identifiers", exclusions="No biological inference"),
|
|
42
|
-
dict(software="orfdata tests", version="1", parameters={} ),
|
|
43
|
-
)
|
|
44
|
-
samples = tuple(Sample("sample", context, biological_unit_id="animal",
|
|
45
|
-
attributes={"source_taxon": context.taxon_id}) for context in contexts)
|
|
46
|
-
assays = tuple(Assay("rna", "sample", "rna_seq", context, technical_replicate="1")
|
|
47
|
-
for context in contexts)
|
|
48
|
-
observations = tuple(Measurement(
|
|
49
|
-
"value", ScopedTarget(context, "gene", "same_id", "ensembl:110"),
|
|
50
|
-
"sample", "rna", "rna_abundance", index, "TPM", "fixture", "1",
|
|
51
|
-
provenance=dict(source_asset="input.tsv", row=index + 1),
|
|
52
|
-
) for index, context in enumerate(contexts))
|
|
53
|
-
return manifest, samples, assays, observations
|
|
54
|
-
|
|
55
|
-
|
|
56
24
|
def write_dataset(path, experiment, **kwargs):
|
|
57
25
|
manifest, samples, assays, observations = experiment
|
|
58
26
|
return QuantitativeDataset.create(path, manifest, samples=samples, assays=assays,
|
|
@@ -77,7 +45,8 @@ def test_four_species_id_collisions_and_scoped_queries(tmp_path, experiment):
|
|
|
77
45
|
assert len(observations) == 4
|
|
78
46
|
assert {o.target.reference.taxon_id for o in observations} == {9606, 10090, 9615, 4932}
|
|
79
47
|
assert {o.dataset_id for o in observations} == {"fixture"}
|
|
80
|
-
assert dataset.counts == dict(sample=4, assay=4, mapping=0, target=4, measurement=4
|
|
48
|
+
assert dataset.counts == dict(sample=4, assay=4, mapping=0, target=4, measurement=4,
|
|
49
|
+
assay_pair=0, target_relation=0)
|
|
81
50
|
for expected in experiment[3]:
|
|
82
51
|
assert list(dataset.measurements(target=expected.target)) == [expected]
|
|
83
52
|
assert list(dataset.measurements(reference=expected.target.reference, sample_id="sample")) == [expected]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""No manufactured sample matches, ORF specificity, replicates or molecule ratios."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from orfdata import (
|
|
10
|
+
Assay,
|
|
11
|
+
AssayPair,
|
|
12
|
+
DatasetRegistry,
|
|
13
|
+
MatchedMeasurements,
|
|
14
|
+
Measurement,
|
|
15
|
+
OrthologyComparison,
|
|
16
|
+
OrthologyMapping,
|
|
17
|
+
QuantitativeDataset,
|
|
18
|
+
ScopedTarget,
|
|
19
|
+
TargetRelation,
|
|
20
|
+
compare_orthologs,
|
|
21
|
+
)
|
|
22
|
+
from orfdata.cli import run
|
|
23
|
+
from orfdata.pairing import PAIRING_BASES
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def paired(experiment, tmp_path):
|
|
28
|
+
manifest, samples, assays, observations = experiment
|
|
29
|
+
proteins, pairs, relations = [], [], []
|
|
30
|
+
assays = list(assays)
|
|
31
|
+
for index, reference in enumerate(manifest.references):
|
|
32
|
+
target = ScopedTarget(reference, "protein", "same_protein", "source-fasta:1")
|
|
33
|
+
assays.append(Assay("protein", "sample", "digest_ms", reference, run_id="run-1",
|
|
34
|
+
technical_replicate="1", channel="126", plex="plex-1", bridge="131C",
|
|
35
|
+
normalization={"method": "source TMT bridge"}))
|
|
36
|
+
proteins.append(Measurement("protein-value", target, "sample", "protein", "protein_abundance",
|
|
37
|
+
100 + index, "iBAQ", "fixture", "1",
|
|
38
|
+
provenance={"source_asset": "input.tsv", "row": index + 9}))
|
|
39
|
+
pairs.append(AssayPair("pair", reference, "fixture", "1", "rna", "protein", "same_material",
|
|
40
|
+
{"source_asset": "input.tsv", "source_pair_id": index}, "concurrent"))
|
|
41
|
+
relations.append(TargetRelation("encoding", "fixture", "1", observations[index].target,
|
|
42
|
+
target, "canonical_translation", "one_to_one",
|
|
43
|
+
{"source_asset": "input.tsv", "method": "test fixture"}))
|
|
44
|
+
values = dict(samples=samples, assays=assays, measurements=(*observations, *proteins),
|
|
45
|
+
pairings=pairs, relations=relations)
|
|
46
|
+
return manifest, values
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create(tmp_path, paired, **changes):
|
|
50
|
+
manifest, values = paired
|
|
51
|
+
return QuantitativeDataset.create(tmp_path / "paired.sqlite", manifest, **dict(values, **changes))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_four_species_native_observations_and_tmt_lineage(tmp_path, paired):
|
|
55
|
+
dataset = create(tmp_path, paired)
|
|
56
|
+
matches = list(dataset.matched_measurements(allowed_bases=["same_material"]))
|
|
57
|
+
assert len(matches) == 4
|
|
58
|
+
for match in matches:
|
|
59
|
+
assert match.left[0].target.kind == "gene"
|
|
60
|
+
assert match.right[0].target.kind == "protein"
|
|
61
|
+
assert match.left[0].target.reference.key == match.right[0].target.reference.key
|
|
62
|
+
assert len(match.left) == len(match.right) == 1
|
|
63
|
+
assert match.left[0].unit == "TPM" and match.right[0].unit == "iBAQ"
|
|
64
|
+
assert AssayPair.from_dict(match.pairing.to_dict()) == match.pairing
|
|
65
|
+
assert TargetRelation.from_dict(match.relation.to_dict()) == match.relation
|
|
66
|
+
assert MatchedMeasurements.from_dict(match.to_dict()) == match
|
|
67
|
+
assay = next(a for a in dataset.assays() if a.kind == "digest_ms")
|
|
68
|
+
assert (assay.plex, assay.channel, assay.bridge, assay.technical_replicate) == ("plex-1", "126", "131C", "1")
|
|
69
|
+
with pytest.raises(TypeError):
|
|
70
|
+
list(dataset.matched_measurements())
|
|
71
|
+
with pytest.raises(ValueError, match="across species"):
|
|
72
|
+
list(dataset.matched_measurements(allowed_bases=["same_material"], pair_ids=["pair"]))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize("basis", PAIRING_BASES)
|
|
76
|
+
def test_pairing_basis_must_be_allowed_explicitly(tmp_path, paired, basis):
|
|
77
|
+
pair = replace(paired[1]["pairings"][0], basis=basis)
|
|
78
|
+
dataset = create(tmp_path, paired, pairings=[pair])
|
|
79
|
+
assert len(list(dataset.matched_measurements(allowed_bases=[basis]))) == 1
|
|
80
|
+
excluded = "same_material" if basis != "same_material" else "independent_cultures"
|
|
81
|
+
assert list(dataset.matched_measurements(allowed_bases=[excluded])) == []
|
|
82
|
+
with pytest.raises(ValueError, match="valid allowed"):
|
|
83
|
+
list(dataset.matched_measurements(allowed_bases=[]))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize("temporal", ("unknown", "longitudinal"))
|
|
87
|
+
def test_time_mismatch_requires_opt_in(tmp_path, paired, temporal):
|
|
88
|
+
pair = replace(paired[1]["pairings"][0], temporal_relation=temporal)
|
|
89
|
+
dataset = create(tmp_path, paired, pairings=[pair])
|
|
90
|
+
assert list(dataset.matched_measurements(allowed_bases=["same_material"])) == []
|
|
91
|
+
assert len(list(dataset.matched_measurements(allowed_bases=["same_material"],
|
|
92
|
+
allowed_temporal=[temporal]))) == 1
|
|
93
|
+
with pytest.raises(ValueError, match="nonzero interval"):
|
|
94
|
+
replace(pair, temporal_relation="concurrent", interval=1, interval_unit="day")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_missing_mates_and_multiple_values_do_not_create_replicates(tmp_path, paired):
|
|
98
|
+
pair = replace(paired[1]["pairings"][0], right_assay_id=None)
|
|
99
|
+
original = paired[1]["measurements"][0]
|
|
100
|
+
extra = replace(original, measurement_id="rna-count", unit="counts", value=99)
|
|
101
|
+
dataset = create(tmp_path, paired, pairings=[pair], measurements=(*paired[1]["measurements"], extra))
|
|
102
|
+
match = next(dataset.matched_measurements(allowed_bases=["same_material"]))
|
|
103
|
+
assert len(match.left) == 2 and match.right == ()
|
|
104
|
+
assert {v.sample_id for v in match.left} == {"sample"}
|
|
105
|
+
assert {v.assay_id for v in match.left} == {"rna"}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_same_tissue_does_not_make_a_pair(tmp_path, paired):
|
|
109
|
+
samples = tuple(replace(sample, tissue="liver") for sample in paired[1]["samples"])
|
|
110
|
+
dataset = create(tmp_path, paired, samples=samples, pairings=[])
|
|
111
|
+
assert list(dataset.matched_measurements(allowed_bases=PAIRING_BASES)) == []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_pairing_cannot_disagree_with_known_donors_or_release(tmp_path, paired):
|
|
115
|
+
sample = replace(paired[1]["samples"][0], sample_id="other", biological_unit_id="other-animal")
|
|
116
|
+
assays = [replace(a, sample_id="other") if a.assay_id == "protein" and a.reference == sample.reference else a
|
|
117
|
+
for a in paired[1]["assays"]]
|
|
118
|
+
with pytest.raises(ValueError, match="biological-unit"):
|
|
119
|
+
create(tmp_path, paired, samples=(*paired[1]["samples"], sample), assays=assays)
|
|
120
|
+
pair = replace(paired[1]["pairings"][0], dataset_version="different")
|
|
121
|
+
with pytest.raises(ValueError, match="different dataset release"):
|
|
122
|
+
create(tmp_path, paired, pairings=[pair])
|
|
123
|
+
pair = replace(paired[1]["pairings"][0], right_assay_id="missing")
|
|
124
|
+
with pytest.raises(sqlite3.IntegrityError):
|
|
125
|
+
create(tmp_path, paired, pairings=[pair])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_native_ratios_keep_units_inputs_and_undefined_denominators(tmp_path, paired):
|
|
129
|
+
dataset = create(tmp_path, paired)
|
|
130
|
+
matches = list(dataset.matched_measurements(allowed_bases=["same_material"]))
|
|
131
|
+
for match in matches:
|
|
132
|
+
numerator, denominator = match.right[0], match.left[0]
|
|
133
|
+
value = match.ratio(numerator, denominator, measurement_id="ratio", method="native_ratio",
|
|
134
|
+
normalization={"protein": "source bridge", "rna": "source TPM"})
|
|
135
|
+
assert value.unit == "(iBAQ)/(TPM)"
|
|
136
|
+
assert value.quantity == "protein_rna_ratio"
|
|
137
|
+
assert value.input_measurements == (numerator.measurement_id, denominator.measurement_id)
|
|
138
|
+
assert value.derivation["uncertainty"] == "not_propagated"
|
|
139
|
+
if denominator.value == 0:
|
|
140
|
+
assert value.value is None and value.status == "not_measured"
|
|
141
|
+
assert value.derivation["unresolved_reason"] == "nonpositive_denominator"
|
|
142
|
+
else:
|
|
143
|
+
assert value.value == numerator.value / denominator.value
|
|
144
|
+
original = matches[0]
|
|
145
|
+
for status in ("below_detection", "missing", "not_measured", "not_mapped"):
|
|
146
|
+
denominator = replace(original.left[0], value=None, status=status)
|
|
147
|
+
match = replace(original, left=(denominator,))
|
|
148
|
+
value = match.ratio(match.right[0], denominator, measurement_id="ratio", method="test",
|
|
149
|
+
normalization={"method": "unchanged"})
|
|
150
|
+
assert value.value is None and value.status == "not_measured"
|
|
151
|
+
logged = replace(original.left[0], scale="log", log_base=2, value=1)
|
|
152
|
+
match = replace(original, left=(logged,))
|
|
153
|
+
with pytest.raises(ValueError, match="must be linear"):
|
|
154
|
+
match.ratio(match.right[0], logged, measurement_id="ratio", method="test",
|
|
155
|
+
normalization={"method": "unchanged"})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_parent_rna_and_ambiguous_groups_cannot_become_orf_ratios(paired):
|
|
159
|
+
pair, relation = paired[1]["pairings"][0], paired[1]["relations"][0]
|
|
160
|
+
left, right = paired[1]["measurements"][0], paired[1]["measurements"][4]
|
|
161
|
+
orf = replace(right.target, kind="orf")
|
|
162
|
+
relation = replace(relation, right=orf, relation="parent_rna_context", cardinality="one_to_many")
|
|
163
|
+
match = MatchedMeasurements(pair, relation, (left,), (replace(right, target=orf),))
|
|
164
|
+
assert match.left[0].target.kind == "gene"
|
|
165
|
+
with pytest.raises(ValueError, match="Parent RNA/group"):
|
|
166
|
+
match.ratio(match.right[0], left, measurement_id="ratio", method="test", normalization={"method": "none"})
|
|
167
|
+
group = replace(left.target, kind="protein_group")
|
|
168
|
+
group_relation = replace(relation, left=group, relation="shared_group_member")
|
|
169
|
+
match = MatchedMeasurements(pair, group_relation, (replace(left, target=group),), match.right)
|
|
170
|
+
with pytest.raises(ValueError, match="Parent RNA/group"):
|
|
171
|
+
match.ratio(match.right[0], match.left[0], measurement_id="ratio", method="test", normalization={"method": "none"})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_duplicate_isoform_attribution_is_not_one_to_one(tmp_path, paired):
|
|
175
|
+
relation = paired[1]["relations"][0]
|
|
176
|
+
other = replace(relation, relation_id="isoform", right=replace(relation.right, identifier="isoform2"))
|
|
177
|
+
with pytest.raises(ValueError, match="Conflicting one-to-one"):
|
|
178
|
+
create(tmp_path, paired, relations=[relation, other])
|
|
179
|
+
with pytest.raises(ValueError, match="Conflicting one-to-one"):
|
|
180
|
+
create(tmp_path, paired, relations=[relation, replace(other, cardinality="one_to_many")])
|
|
181
|
+
pair = paired[1]["pairings"][0]
|
|
182
|
+
left, right = paired[1]["measurements"][0], paired[1]["measurements"][4]
|
|
183
|
+
match = MatchedMeasurements(pair, replace(relation, cardinality="one_to_many"), (left,), (right,))
|
|
184
|
+
with pytest.raises(ValueError, match="one-to-one attribution"):
|
|
185
|
+
match.ratio(right, left, measurement_id="ratio", method="test", normalization={"method": "none"})
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_gene_and_transcript_links_are_distinct_attribution_axes(tmp_path, paired):
|
|
189
|
+
gene_relation = paired[1]["relations"][0]
|
|
190
|
+
transcript_relation = replace(gene_relation, relation_id="transcript-encoding",
|
|
191
|
+
left=replace(gene_relation.left, kind="transcript", identifier="rna"))
|
|
192
|
+
dataset = create(tmp_path, paired, relations=[gene_relation, transcript_relation])
|
|
193
|
+
assert len(list(dataset.relations())) == 2
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_same_target_across_samples_stays_a_separate_comparison(tmp_path, paired):
|
|
197
|
+
manifest, values = paired
|
|
198
|
+
original = values["measurements"][0]
|
|
199
|
+
sample = replace(values["samples"][0], sample_id="second", biological_unit_id="animal-2")
|
|
200
|
+
assay = Assay("rna-2", "second", "rna_seq", sample.reference)
|
|
201
|
+
other = replace(original, measurement_id="second-value", sample_id="second", assay_id="rna-2", value=2)
|
|
202
|
+
relation = TargetRelation("same", "fixture", "1", original.target, original.target,
|
|
203
|
+
"same_target", "one_to_one", {"identity": "exact scoped target"})
|
|
204
|
+
pair = replace(values["pairings"][0], right_assay_id="rna-2", basis="biological_replicate")
|
|
205
|
+
dataset = create(tmp_path, paired, samples=(*values["samples"], sample),
|
|
206
|
+
assays=(*values["assays"], assay), measurements=(*values["measurements"], other),
|
|
207
|
+
pairings=[pair], relations=[relation])
|
|
208
|
+
match = next(dataset.matched_measurements(allowed_bases=["biological_replicate"]))
|
|
209
|
+
assert match.left[0].target == match.right[0].target
|
|
210
|
+
assert match.left[0].sample_id != match.right[0].sample_id
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_old_storage_has_no_inferred_relationships(tmp_path, paired):
|
|
214
|
+
dataset = create(tmp_path, paired)
|
|
215
|
+
with sqlite3.connect(dataset.path) as connection:
|
|
216
|
+
connection.execute("DROP TABLE assay_pair")
|
|
217
|
+
connection.execute("DROP TABLE target_relation")
|
|
218
|
+
connection.execute("PRAGMA user_version=1")
|
|
219
|
+
old = QuantitativeDataset(dataset.path)
|
|
220
|
+
assert list(old.pairings()) == list(old.relations()) == []
|
|
221
|
+
assert list(old.matched_measurements(allowed_bases=PAIRING_BASES)) == []
|
|
222
|
+
assert len(list(old.measurements())) == 8
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_orthology_keeps_both_species_and_never_transfers_abundance(tmp_path, paired):
|
|
226
|
+
dataset = create(tmp_path, paired)
|
|
227
|
+
observations = paired[1]["measurements"]
|
|
228
|
+
mappings = [OrthologyMapping(str(index), observations[0].target, observations[index].target,
|
|
229
|
+
paired[0].sources[0], "test:1", "one_to_one",
|
|
230
|
+
{"definition": "synthetic fixture, not biological evidence"},
|
|
231
|
+
{"method": "test fixture"}) for index in (1, 2, 3)]
|
|
232
|
+
comparisons = list(compare_orthologs(dataset, dataset, mappings))
|
|
233
|
+
assert len(comparisons) == 3
|
|
234
|
+
for index, comparison in enumerate(comparisons, 1):
|
|
235
|
+
assert comparison.left == (observations[0],)
|
|
236
|
+
assert comparison.right == (observations[index],)
|
|
237
|
+
assert comparison.left[0].value != comparison.right[0].value
|
|
238
|
+
assert OrthologyMapping.from_dict(comparison.mapping.to_dict()) == comparison.mapping
|
|
239
|
+
assert OrthologyComparison.from_dict(comparison.to_dict()) == comparison
|
|
240
|
+
assert list(compare_orthologs(dataset, dataset, [replace(mappings[0], cardinality="one_to_many")])) == []
|
|
241
|
+
alternate = replace(mappings[0], mapping_id="other", right=replace(mappings[0].right, identifier="other"))
|
|
242
|
+
with pytest.raises(ValueError, match="Conflicting one-to-one"):
|
|
243
|
+
list(compare_orthologs(dataset, dataset, [mappings[0], alternate]))
|
|
244
|
+
with pytest.raises(ValueError, match="sequence, frame and position"):
|
|
245
|
+
replace(mappings[0], left=replace(mappings[0].left, kind="orf"), right=replace(mappings[0].right, kind="orf"))
|
|
246
|
+
orf_mapping = replace(mappings[0], left=replace(mappings[0].left, kind="orf"),
|
|
247
|
+
right=replace(mappings[0].right, kind="orf"),
|
|
248
|
+
evidence={"sequence": "pinned alignment", "frame": "same frame", "position": "synteny"})
|
|
249
|
+
assert list(compare_orthologs(dataset, dataset, [orf_mapping])) == []
|
|
250
|
+
comparison = next(compare_orthologs(dataset, dataset, [orf_mapping], allow_orfs=True))
|
|
251
|
+
assert comparison.left == comparison.right == () # Gene values are not ORF values.
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_paired_table_cli_preserves_rna_protein_and_missing_ribo(tmp_path, paired, capsys):
|
|
255
|
+
from test_measurements import definition
|
|
256
|
+
|
|
257
|
+
dataset = create(tmp_path, paired)
|
|
258
|
+
metadata = definition(dataset)
|
|
259
|
+
cache_root = tmp_path / "cache"
|
|
260
|
+
DatasetRegistry(metadata, cache_root=cache_root).download("fixture", show_progress=False)
|
|
261
|
+
registry_path = tmp_path / "registry.json"
|
|
262
|
+
registry_path.write_text(json.dumps(metadata))
|
|
263
|
+
run(["datasets", "--registry", str(registry_path), "--cache-root", str(cache_root),
|
|
264
|
+
"pairs", "fixture", "--basis", "same_material"])
|
|
265
|
+
rows = [json.loads(line) for line in capsys.readouterr().out.splitlines()]
|
|
266
|
+
assert len(rows) == 4
|
|
267
|
+
assert all(row["ribosome_occupancy"] == [] for row in rows)
|
|
268
|
+
assert all(row["rna_abundance"][0]["unit"] == "TPM" for row in rows)
|
|
269
|
+
assert all(row["protein_abundance"][0]["unit"] == "iBAQ" for row in rows)
|
orfdata-0.3.0/orfdata/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|