orfdata 0.4.0__tar.gz → 0.5.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.4.0/orfdata.egg-info → orfdata-0.5.0}/PKG-INFO +1 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/docs/measurements.md +8 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/docs/pairing.md +15 -2
- {orfdata-0.4.0 → orfdata-0.5.0}/docs/reference.md +2 -3
- orfdata-0.5.0/docs/source-references.md +90 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/__init__.py +13 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/dataset.py +116 -26
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/dataset_cli.py +19 -4
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/measurements.py +47 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/pairing.py +41 -8
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/reference.py +32 -7
- orfdata-0.5.0/orfdata/reference_mapping.py +150 -0
- orfdata-0.5.0/orfdata/version.py +1 -0
- {orfdata-0.4.0 → orfdata-0.5.0/orfdata.egg-info}/PKG-INFO +1 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata.egg-info/SOURCES.txt +5 -0
- orfdata-0.5.0/tests/data/references/context-keys-v0.4.json +62 -0
- orfdata-0.5.0/tests/test_cross_reference_pairing.py +160 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_measurements.py +1 -1
- orfdata-0.5.0/tests/test_reference_mapping.py +225 -0
- orfdata-0.4.0/orfdata/version.py +0 -1
- {orfdata-0.4.0 → orfdata-0.5.0}/LICENSE +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/MANIFEST.in +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/README.md +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/deploy.sh +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/docs/design.md +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/docs/species.md +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/lint.sh +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/build.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/catalog.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/cli.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/datasets.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/manifest.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/datasets.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/initiation_context.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/model.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata/sequence.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata.egg-info/dependency_links.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata.egg-info/entry_points.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata.egg-info/requires.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/orfdata.egg-info/top_level.txt +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/pyproject.toml +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/setup.cfg +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/test.sh +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/conftest.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/data/references/README.md +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/data/references/homo_sapiens.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/data/references/mus_musculus.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_initiation_context.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_pairing.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_reference_context.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_reference_orf.py +0 -0
- {orfdata-0.4.0 → orfdata-0.5.0}/tests/test_uorf_catalog.py +0 -0
|
@@ -46,6 +46,10 @@ mapping table. The default PyEnsembl target uses its unversioned `id`; request
|
|
|
46
46
|
mapping across annotation releases, gene isoforms, ORFs or species. A variant or
|
|
47
47
|
fusion product does not inherit a native reference protein measurement.
|
|
48
48
|
|
|
49
|
+
For native sources with unreported genomic metadata, use explicit
|
|
50
|
+
[source references and mapping views](source-references.md) to retain the original
|
|
51
|
+
observation alongside its mapping to a PyEnsembl target.
|
|
52
|
+
|
|
49
53
|
The CLI uses the same operations:
|
|
50
54
|
|
|
51
55
|
```sh
|
|
@@ -96,7 +100,10 @@ An importer calls `QuantitativeDataset.create(path, manifest, samples=...,
|
|
|
96
100
|
assays=..., measurements=..., mappings=...)`. These inputs may be generators;
|
|
97
101
|
measurements are written incrementally. Every observation names a source asset
|
|
98
102
|
or earlier input measurements. Derived inputs precede outputs in the same
|
|
99
|
-
|
|
103
|
+
dataset release. `input_measurements` names IDs within the output reference;
|
|
104
|
+
`input_references` instead uses `MeasurementReference` records with each input's
|
|
105
|
+
dataset, version, reference and ID. The two forms cannot be mixed. Cross-species
|
|
106
|
+
derived abundance is rejected. Duplicate IDs, broken foreign keys, wrong dataset releases
|
|
100
107
|
and cross-reference links fail the import. Failed builds do not replace data.
|
|
101
108
|
|
|
102
109
|
Use `build_dataset(manifest, builder, ...)` for a managed local import.
|
|
@@ -25,6 +25,13 @@ only explicitly concurrent assays are returned; opt into `longitudinal` or
|
|
|
25
25
|
TMT channels and bridges remain in the assays and do not become biological
|
|
26
26
|
sample counts.
|
|
27
27
|
|
|
28
|
+
RNA annotation and protein search references can differ. `AssayPair.reference`
|
|
29
|
+
is the left assay's context; `right_reference` explicitly identifies the right
|
|
30
|
+
assay's context and defaults to the left for older records. Both must belong to
|
|
31
|
+
the same species. A protein FASTA does not need an invented genomic assembly:
|
|
32
|
+
see [source references](source-references.md). Original targets and references
|
|
33
|
+
remain attached to both sides, even when their local IDs happen to match.
|
|
34
|
+
|
|
28
35
|
The CLI streams the same table rows:
|
|
29
36
|
|
|
30
37
|
```sh
|
|
@@ -39,10 +46,13 @@ and cardinality. `same_target` compares one target across assays/samples.
|
|
|
39
46
|
`parent_rna_context` links parent RNA to an ORF but keeps the RNA value attached
|
|
40
47
|
to its original gene/transcript. `shared_group_member` records an ambiguous
|
|
41
48
|
protein-group assignment; the group value stays a group value.
|
|
49
|
+
`gene_protein_group_context` links RNA to an ambiguous source protein group
|
|
50
|
+
without assigning that group's abundance to an individual protein or ORF.
|
|
42
51
|
|
|
43
52
|
Importers pass `AssayPair` and `TargetRelation` iterators as `pairings=` and
|
|
44
53
|
`relations=` to `QuantitativeDataset.create`. IDs belong to the dataset release
|
|
45
|
-
and reference context.
|
|
54
|
+
and left reference context. The right assay/target keeps its own context.
|
|
55
|
+
Invalid assay references, contradictory known donor
|
|
46
56
|
identities and conflicting one-to-one assignments fail the import. Original
|
|
47
57
|
storage-version-1 datasets remain readable and have no declared relationships.
|
|
48
58
|
|
|
@@ -67,7 +77,10 @@ assays. Missing, censored or nonpositive denominators produce an unresolved
|
|
|
67
77
|
`not_measured` result with its reason, never infinity or a measured zero. RPF/RNA
|
|
68
78
|
ratios are source-defined translation-efficiency measures, not measured flux.
|
|
69
79
|
All derived values retain their input IDs, pairing, target attribution and
|
|
70
|
-
normalization assumptions.
|
|
80
|
+
normalization assumptions. When input reference contexts differ, the ratio
|
|
81
|
+
stores `MeasurementReference` records in `input_references`, rather than
|
|
82
|
+
unqualified local IDs. This preserves source identity across annotation and
|
|
83
|
+
search-FASTA releases.
|
|
71
84
|
|
|
72
85
|
## Across species
|
|
73
86
|
|
|
@@ -75,9 +75,8 @@ reconstructed RNA products and preserves each candidate's RNA support. For
|
|
|
75
75
|
example, the native TPST1 peptide KIILSKISV is absent from the Sid TPST1–CRCP
|
|
76
76
|
alternatives, so its MS evidence does not support those altered products.
|
|
77
77
|
|
|
78
|
-
The reference integration
|
|
79
|
-
|
|
80
|
-
with Varcode 10.13.0 and Isovar 1.51.0.
|
|
78
|
+
The reference integration, including strict reference-context validation, is
|
|
79
|
+
available in Varcode 10.13.0 and Isovar 1.51.0.
|
|
81
80
|
|
|
82
81
|
## Data and limits
|
|
83
82
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Source references and PyEnsembl mappings
|
|
2
|
+
|
|
3
|
+
A proteomics experiment may search a known protein FASTA without naming a
|
|
4
|
+
genomic assembly. An older RNA study may report its annotation provider but
|
|
5
|
+
omit the release. ORFdata keeps these source identities and their unknown fields.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from orfdata import ReferenceContext
|
|
9
|
+
|
|
10
|
+
source = ReferenceContext(
|
|
11
|
+
taxon_id=9615, species="dog", genome_build=None,
|
|
12
|
+
annotation_name="uniprot", annotation_version=None,
|
|
13
|
+
sequence_sha256=searched_fasta_sha256,
|
|
14
|
+
)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Unknown assembly/provider fields require a pinned `sequence_sha256` or
|
|
18
|
+
`source_sha256`. The former identifies a sequence set; the latter can pin an
|
|
19
|
+
input table when its reference release is unreported. These source contexts
|
|
20
|
+
cannot bind a genomic transcript. A `ReferenceORF` still requires a complete
|
|
21
|
+
genomic model validated against its actual PyEnsembl transcript. Existing
|
|
22
|
+
complete-reference keys are unchanged.
|
|
23
|
+
|
|
24
|
+
## Query through an explicit mapping
|
|
25
|
+
|
|
26
|
+
`ReferenceMapping` connects a native `ScopedTarget` to all candidate targets in
|
|
27
|
+
one destination reference. It records the dataset/version, mapping method,
|
|
28
|
+
validation, source provenance, status and unresolved/unmapped reason. Importers
|
|
29
|
+
must perform the stated checks against the pinned inputs. Unique identifier
|
|
30
|
+
resolution is separate from sequence validation.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from orfdata import ScopedTarget
|
|
34
|
+
|
|
35
|
+
target = ScopedTarget.from_pyensembl(gene, kind="gene", namespace="ensembl:110")
|
|
36
|
+
for view in dataset.mapped_measurements(target):
|
|
37
|
+
print(view.measurement.value, view.measurement.unit)
|
|
38
|
+
print(view.measurement.target, view.requested_target)
|
|
39
|
+
print(view.mapping.method, view.mapping.validation)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The view contains the **original observation**, including its source reference,
|
|
43
|
+
target, sample, assay, unit and provenance. Nothing is relabeled or copied onto
|
|
44
|
+
the requested target. Direct `dataset.measurements(target=...)` still returns
|
|
45
|
+
only observations native to that exact target/reference.
|
|
46
|
+
|
|
47
|
+
By default, mapped queries return uniquely resolved assignments. Use
|
|
48
|
+
`mapping_statuses=["ambiguous", "unresolved"]` to inspect other candidates, and
|
|
49
|
+
`require_sequence=True` to require recorded exact sequence validation. A group
|
|
50
|
+
value returned for a candidate member has `attribution="group_membership_context"`;
|
|
51
|
+
it does not independently quantify that protein. Parent gene/transcript RNA
|
|
52
|
+
cannot map directly onto an ORF. Use a separate parent-context relationship.
|
|
53
|
+
|
|
54
|
+
The CLI exposes the same query:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
orfdata datasets query DATASET --mapped --reference ref:SHA256 \
|
|
58
|
+
--kind protein --namespace ensembl:110 --target PROTEIN_ID --require-sequence
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Importer contract
|
|
62
|
+
|
|
63
|
+
Pass `reference_mappings=` to `QuantitativeDataset.create`. Declare source and
|
|
64
|
+
destination contexts in the manifest. Each source/destination-reference pair
|
|
65
|
+
has one mapping containing all candidates, preventing competing unique claims.
|
|
66
|
+
Unmapped records remain visible through `dataset.reference_mappings()`.
|
|
67
|
+
|
|
68
|
+
The status is `unique`, `ambiguous`, `unmapped` or `unresolved`. A single reported
|
|
69
|
+
protein representative with incomplete group membership is unresolved; its
|
|
70
|
+
measurement cannot become a uniquely measured member. Identifier-based mappings
|
|
71
|
+
must say what was checked and leave sequence validation absent when unavailable.
|
|
72
|
+
For an exact sequence search, record:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
validation = {"sequence": {
|
|
76
|
+
"source_sha256": source_protein_sequence_sha256,
|
|
77
|
+
"target_sha256": matching_protein_sequence_sha256,
|
|
78
|
+
"all_candidates_checked": True,
|
|
79
|
+
}}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
These are hashes of the compared sequences, distinct from the whole searched
|
|
83
|
+
FASTA's hash in the reference context. Both hashes must agree to count as an
|
|
84
|
+
exact match. ORF assignments additionally require their own `frame` and
|
|
85
|
+
`position` evidence. Known reference-strain differences need explicit
|
|
86
|
+
`strain_relationship` evidence. Cross-species mappings use the separate
|
|
87
|
+
[orthology API](pairing.md#across-species).
|
|
88
|
+
|
|
89
|
+
Storage version 3 preserves these mappings. Earlier versions remain readable
|
|
90
|
+
and report no declared reference mappings; no mapping is inferred on load.
|
|
@@ -15,7 +15,15 @@ from .catalog import (
|
|
|
15
15
|
from .dataset import QuantitativeDataset
|
|
16
16
|
from .datasets import DatasetRegistry, build_dataset
|
|
17
17
|
from .initiation_context import KOZAK_PREFERENCES, InitiationContext, annotate_initiation_context
|
|
18
|
-
from .measurements import
|
|
18
|
+
from .measurements import (
|
|
19
|
+
Assay,
|
|
20
|
+
DatasetManifest,
|
|
21
|
+
Measurement,
|
|
22
|
+
MeasurementReference,
|
|
23
|
+
Sample,
|
|
24
|
+
SourceAsset,
|
|
25
|
+
TargetMapping,
|
|
26
|
+
)
|
|
19
27
|
from .model import ORFModel, ReferenceORF
|
|
20
28
|
from .pairing import (
|
|
21
29
|
AssayPair,
|
|
@@ -26,6 +34,7 @@ from .pairing import (
|
|
|
26
34
|
compare_orthologs,
|
|
27
35
|
)
|
|
28
36
|
from .reference import ReferenceContext, ScopedTarget
|
|
37
|
+
from .reference_mapping import MappedMeasurement, ReferenceMapping
|
|
29
38
|
from .version import __version__
|
|
30
39
|
|
|
31
40
|
__all__ = [
|
|
@@ -47,6 +56,7 @@ __all__ = [
|
|
|
47
56
|
"Sample",
|
|
48
57
|
"Assay",
|
|
49
58
|
"Measurement",
|
|
59
|
+
"MeasurementReference",
|
|
50
60
|
"TargetMapping",
|
|
51
61
|
"AssayPair",
|
|
52
62
|
"TargetRelation",
|
|
@@ -54,6 +64,8 @@ __all__ = [
|
|
|
54
64
|
"OrthologyMapping",
|
|
55
65
|
"OrthologyComparison",
|
|
56
66
|
"compare_orthologs",
|
|
67
|
+
"ReferenceMapping",
|
|
68
|
+
"MappedMeasurement",
|
|
57
69
|
"load_catalog",
|
|
58
70
|
"UORFCatalog",
|
|
59
71
|
"UORFRecord",
|
|
@@ -13,6 +13,7 @@ from .measurements import (
|
|
|
13
13
|
Assay,
|
|
14
14
|
DatasetManifest,
|
|
15
15
|
Measurement,
|
|
16
|
+
MeasurementReference,
|
|
16
17
|
Sample,
|
|
17
18
|
TargetMapping,
|
|
18
19
|
)
|
|
@@ -25,6 +26,7 @@ from .pairing import (
|
|
|
25
26
|
_check_one_to_one,
|
|
26
27
|
)
|
|
27
28
|
from .reference import ReferenceContext, ScopedTarget
|
|
29
|
+
from .reference_mapping import MappedMeasurement, ReferenceMapping
|
|
28
30
|
|
|
29
31
|
_SCHEMA = """
|
|
30
32
|
CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
@@ -52,25 +54,41 @@ CREATE TABLE measurement (
|
|
|
52
54
|
FOREIGN KEY(reference_key, assay_id, sample_id)
|
|
53
55
|
REFERENCES assay(reference_key, id, sample_id));
|
|
54
56
|
CREATE TABLE input_measurement (
|
|
55
|
-
reference_key TEXT NOT NULL, output_id TEXT NOT NULL,
|
|
56
|
-
|
|
57
|
+
reference_key TEXT NOT NULL, output_id TEXT NOT NULL,
|
|
58
|
+
input_reference_key TEXT NOT NULL, input_id TEXT NOT NULL,
|
|
59
|
+
PRIMARY KEY(reference_key, output_id, input_reference_key, input_id),
|
|
57
60
|
FOREIGN KEY(reference_key, output_id) REFERENCES measurement(reference_key, id),
|
|
58
|
-
FOREIGN KEY(
|
|
61
|
+
FOREIGN KEY(input_reference_key, input_id) REFERENCES measurement(reference_key, id));
|
|
59
62
|
CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
|
|
60
63
|
CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
|
|
61
64
|
CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
|
|
62
65
|
CREATE TABLE assay_pair (
|
|
63
66
|
reference_key TEXT NOT NULL REFERENCES reference(key), id TEXT NOT NULL,
|
|
67
|
+
right_reference_key TEXT NOT NULL REFERENCES reference(key),
|
|
64
68
|
left_assay_id TEXT, right_assay_id TEXT, record TEXT NOT NULL,
|
|
65
69
|
PRIMARY KEY(reference_key, id),
|
|
66
70
|
FOREIGN KEY(reference_key, left_assay_id) REFERENCES assay(reference_key, id),
|
|
67
|
-
FOREIGN KEY(
|
|
71
|
+
FOREIGN KEY(right_reference_key, right_assay_id) REFERENCES assay(reference_key, id));
|
|
68
72
|
CREATE TABLE target_relation (
|
|
69
73
|
reference_key TEXT NOT NULL, id TEXT NOT NULL,
|
|
74
|
+
right_reference_key TEXT NOT NULL,
|
|
70
75
|
left_target_key TEXT NOT NULL, right_target_key TEXT NOT NULL, record TEXT NOT NULL,
|
|
71
76
|
PRIMARY KEY(reference_key, id),
|
|
72
77
|
FOREIGN KEY(reference_key, left_target_key) REFERENCES target(reference_key, key),
|
|
73
|
-
FOREIGN KEY(
|
|
78
|
+
FOREIGN KEY(right_reference_key, right_target_key) REFERENCES target(reference_key, key));
|
|
79
|
+
CREATE TABLE reference_mapping (
|
|
80
|
+
reference_key TEXT NOT NULL, id TEXT NOT NULL, source_target_key TEXT NOT NULL,
|
|
81
|
+
target_reference_key TEXT NOT NULL REFERENCES reference(key), record TEXT NOT NULL,
|
|
82
|
+
PRIMARY KEY(reference_key, id),
|
|
83
|
+
UNIQUE(reference_key, source_target_key, target_reference_key),
|
|
84
|
+
FOREIGN KEY(reference_key, source_target_key) REFERENCES target(reference_key, key));
|
|
85
|
+
CREATE TABLE reference_assignment (
|
|
86
|
+
source_reference_key TEXT NOT NULL, mapping_id TEXT NOT NULL,
|
|
87
|
+
target_reference_key TEXT NOT NULL, target_key TEXT NOT NULL,
|
|
88
|
+
PRIMARY KEY(source_reference_key, mapping_id, target_reference_key, target_key),
|
|
89
|
+
FOREIGN KEY(source_reference_key, mapping_id) REFERENCES reference_mapping(reference_key, id),
|
|
90
|
+
FOREIGN KEY(target_reference_key, target_key) REFERENCES target(reference_key, key));
|
|
91
|
+
CREATE INDEX reference_assignment_lookup ON reference_assignment(target_reference_key, target_key);
|
|
74
92
|
"""
|
|
75
93
|
|
|
76
94
|
|
|
@@ -94,7 +112,7 @@ class QuantitativeDataset:
|
|
|
94
112
|
self.path = Path(path).resolve()
|
|
95
113
|
with closing(self._connect()) as connection:
|
|
96
114
|
self._storage_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
|
97
|
-
if self._storage_version not in (1, 2):
|
|
115
|
+
if self._storage_version not in (1, 2, 3):
|
|
98
116
|
raise ValueError("Unsupported quantitative database schema")
|
|
99
117
|
row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
|
|
100
118
|
if row is None:
|
|
@@ -110,7 +128,7 @@ class QuantitativeDataset:
|
|
|
110
128
|
|
|
111
129
|
@classmethod
|
|
112
130
|
def create(cls, path, manifest, *, samples, assays, measurements, mappings=(),
|
|
113
|
-
pairings=(), relations=()):
|
|
131
|
+
pairings=(), relations=(), reference_mappings=()):
|
|
114
132
|
"""Write a new dataset from iterators, validating scope and foreign keys.
|
|
115
133
|
|
|
116
134
|
Never overwrite an existing file. An interrupted/invalid build removes
|
|
@@ -128,7 +146,7 @@ class QuantitativeDataset:
|
|
|
128
146
|
connection = sqlite3.connect(path)
|
|
129
147
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
130
148
|
connection.executescript(_SCHEMA)
|
|
131
|
-
connection.execute("PRAGMA user_version=
|
|
149
|
+
connection.execute("PRAGMA user_version=3")
|
|
132
150
|
connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
|
|
133
151
|
(_json(manifest.to_dict()),))
|
|
134
152
|
for reference in manifest.references:
|
|
@@ -154,19 +172,20 @@ class QuantitativeDataset:
|
|
|
154
172
|
raise ValueError("Pairing belongs to a different dataset release")
|
|
155
173
|
if pair.basis in ("same_material", "adjacent_donor_tissue"):
|
|
156
174
|
units = []
|
|
157
|
-
for identifier in (pair.
|
|
175
|
+
for reference, identifier in ((pair.reference, pair.left_assay_id),
|
|
176
|
+
(pair.right_reference, pair.right_assay_id)):
|
|
158
177
|
row = connection.execute(
|
|
159
178
|
"SELECT s.record FROM assay a JOIN sample s ON a.reference_key=s.reference_key "
|
|
160
179
|
"AND a.sample_id=s.id WHERE a.reference_key=? AND a.id=?",
|
|
161
|
-
(
|
|
180
|
+
(reference.key, identifier)).fetchone()
|
|
162
181
|
if row:
|
|
163
182
|
unit = json.loads(row[0]).get("biological_unit_id")
|
|
164
183
|
if unit is not None:
|
|
165
184
|
units.append(unit)
|
|
166
185
|
if len(set(units)) > 1:
|
|
167
186
|
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,
|
|
187
|
+
connection.execute("INSERT INTO assay_pair VALUES (?, ?, ?, ?, ?, ?)",
|
|
188
|
+
(pair.reference.key, pair.pair_id, pair.right_reference.key, pair.left_assay_id,
|
|
170
189
|
pair.right_assay_id, _json(pair.to_dict())))
|
|
171
190
|
relation_links = ({}, {})
|
|
172
191
|
for relation in relations:
|
|
@@ -180,10 +199,11 @@ class QuantitativeDataset:
|
|
|
180
199
|
target.namespace, target.identifier))
|
|
181
200
|
for index, (first, second) in enumerate(((relation.left, relation.right), (relation.right, relation.left))):
|
|
182
201
|
_check_one_to_one(relation_links[index],
|
|
183
|
-
(relation.relation, first.key, second.kind, second.namespace), second.key,
|
|
202
|
+
(relation.relation, first.key, second.reference.key, second.kind, second.namespace), second.key,
|
|
184
203
|
relation.cardinality == "one_to_one")
|
|
185
|
-
connection.execute("INSERT INTO target_relation VALUES (?, ?, ?, ?, ?)",
|
|
204
|
+
connection.execute("INSERT INTO target_relation VALUES (?, ?, ?, ?, ?, ?)",
|
|
186
205
|
(relation.left.reference.key, relation.relation_id,
|
|
206
|
+
relation.right.reference.key,
|
|
187
207
|
_target_key(relation.left), _target_key(relation.right),
|
|
188
208
|
_json(relation.to_dict())))
|
|
189
209
|
for mapping in mappings:
|
|
@@ -192,6 +212,21 @@ class QuantitativeDataset:
|
|
|
192
212
|
connection.execute("INSERT INTO mapping VALUES (?, ?, ?)",
|
|
193
213
|
(mapping.reference.key, mapping.mapping_id,
|
|
194
214
|
_json(mapping.to_dict())))
|
|
215
|
+
for mapping in reference_mappings:
|
|
216
|
+
if not isinstance(mapping, ReferenceMapping):
|
|
217
|
+
raise TypeError("reference_mappings must contain ReferenceMappings")
|
|
218
|
+
if (mapping.dataset_id, mapping.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
219
|
+
raise ValueError("Reference mapping belongs to a different dataset release")
|
|
220
|
+
for target in (mapping.source, *mapping.targets):
|
|
221
|
+
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
222
|
+
(target.reference.key, _target_key(target), target.kind,
|
|
223
|
+
target.namespace, target.identifier))
|
|
224
|
+
connection.execute("INSERT INTO reference_mapping VALUES (?, ?, ?, ?, ?)",
|
|
225
|
+
(mapping.source.reference.key, mapping.mapping_id, _target_key(mapping.source),
|
|
226
|
+
mapping.target_reference.key, _json(mapping.to_dict())))
|
|
227
|
+
connection.executemany("INSERT INTO reference_assignment VALUES (?, ?, ?, ?)",
|
|
228
|
+
((mapping.source.reference.key, mapping.mapping_id,
|
|
229
|
+
target.reference.key, _target_key(target)) for target in mapping.targets))
|
|
195
230
|
source_ids = {s.asset_id for s in manifest.sources}
|
|
196
231
|
for observation in measurements:
|
|
197
232
|
if not isinstance(observation, Measurement):
|
|
@@ -201,15 +236,17 @@ class QuantitativeDataset:
|
|
|
201
236
|
source = observation.provenance.get("source_asset")
|
|
202
237
|
if source is not None and source not in source_ids:
|
|
203
238
|
raise ValueError("Measurement provenance names an unknown source asset")
|
|
204
|
-
if source is None and not observation.input_measurements:
|
|
239
|
+
if source is None and not (observation.input_measurements or observation.input_references):
|
|
205
240
|
raise ValueError("Measurement requires a source_asset or input measurements")
|
|
206
241
|
reference_key = observation.target.reference.key
|
|
207
|
-
for identifier in observation.input_measurements
|
|
242
|
+
inputs = [(reference_key, identifier) for identifier in observation.input_measurements]
|
|
243
|
+
inputs.extend((value.reference.key, value.measurement_id) for value in observation.input_references)
|
|
244
|
+
for input_reference, identifier in inputs:
|
|
208
245
|
if connection.execute(
|
|
209
246
|
"SELECT 1 FROM measurement WHERE reference_key=? AND id=?",
|
|
210
|
-
(
|
|
247
|
+
(input_reference, identifier),
|
|
211
248
|
).fetchone() is None:
|
|
212
|
-
raise ValueError("Derived inputs must precede outputs in
|
|
249
|
+
raise ValueError("Derived inputs must precede outputs in their declared reference contexts")
|
|
213
250
|
target = observation.target
|
|
214
251
|
target_key = _target_key(target)
|
|
215
252
|
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
@@ -221,12 +258,12 @@ class QuantitativeDataset:
|
|
|
221
258
|
(reference_key, observation.measurement_id, target_key,
|
|
222
259
|
observation.sample_id, observation.assay_id,
|
|
223
260
|
observation.quantity, observation.status, _json(record)))
|
|
224
|
-
connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?)",
|
|
225
|
-
((reference_key, observation.measurement_id, identifier)
|
|
226
|
-
for identifier in
|
|
261
|
+
connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?, ?)",
|
|
262
|
+
((reference_key, observation.measurement_id, input_reference, identifier)
|
|
263
|
+
for input_reference, identifier in inputs))
|
|
227
264
|
counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
|
|
228
265
|
for table in ("sample", "assay", "mapping", "target", "measurement",
|
|
229
|
-
"assay_pair", "target_relation")}
|
|
266
|
+
"assay_pair", "target_relation", "reference_mapping")}
|
|
230
267
|
connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
|
|
231
268
|
connection.commit()
|
|
232
269
|
connection.close()
|
|
@@ -267,6 +304,54 @@ class QuantitativeDataset:
|
|
|
267
304
|
def mappings(self, *, reference=None):
|
|
268
305
|
return self._records("mapping", TargetMapping, reference=reference)
|
|
269
306
|
|
|
307
|
+
def reference_mappings(self, *, source_reference=None, target=None):
|
|
308
|
+
"""Iterate original source-to-reference mappings, including unresolved IDs."""
|
|
309
|
+
scope = self._scope(source_reference)
|
|
310
|
+
if target is not None:
|
|
311
|
+
if not isinstance(target, ScopedTarget):
|
|
312
|
+
raise TypeError("target must be a ScopedTarget")
|
|
313
|
+
self._scope(target.reference)
|
|
314
|
+
if self._storage_version < 3:
|
|
315
|
+
return
|
|
316
|
+
query = "SELECT m.record FROM reference_mapping m"
|
|
317
|
+
conditions, params = [], []
|
|
318
|
+
if target is not None:
|
|
319
|
+
query += (" JOIN reference_assignment a ON m.reference_key=a.source_reference_key "
|
|
320
|
+
"AND m.id=a.mapping_id")
|
|
321
|
+
conditions.extend(("a.target_reference_key=?", "a.target_key=?"))
|
|
322
|
+
params.extend((target.reference.key, _target_key(target)))
|
|
323
|
+
if scope is not None:
|
|
324
|
+
conditions.append("m.reference_key=?")
|
|
325
|
+
params.append(scope)
|
|
326
|
+
if conditions:
|
|
327
|
+
query += " WHERE " + " AND ".join(conditions)
|
|
328
|
+
query += " ORDER BY m.reference_key, m.id"
|
|
329
|
+
with closing(self._connect()) as connection:
|
|
330
|
+
for row in connection.execute(query, params):
|
|
331
|
+
yield ReferenceMapping.from_dict(json.loads(row[0]))
|
|
332
|
+
|
|
333
|
+
def mapped_measurements(self, target, *, mapping_statuses=("unique",),
|
|
334
|
+
require_sequence=False, **observation_filters):
|
|
335
|
+
"""Return original values plus mappings, never relabeled measurements.
|
|
336
|
+
|
|
337
|
+
Ambiguous or unresolved candidate membership requires explicit status
|
|
338
|
+
opt-in. Gene RNA cannot map to ORF abundance. require_sequence excludes
|
|
339
|
+
identifier-only mappings; ORF assignments always require their own
|
|
340
|
+
sequence/frame/position validation when the mapping is constructed.
|
|
341
|
+
"""
|
|
342
|
+
allowed = set(mapping_statuses)
|
|
343
|
+
if not allowed or allowed - {"unique", "ambiguous", "unresolved"}:
|
|
344
|
+
raise ValueError("Choose explicit usable mapping statuses")
|
|
345
|
+
if type(require_sequence) is not bool:
|
|
346
|
+
raise TypeError("require_sequence must be a boolean")
|
|
347
|
+
if set(observation_filters) - {"sample_id", "assay_id", "quantity", "status"}:
|
|
348
|
+
raise ValueError("Mapped observation filters cannot override source/target reference identity")
|
|
349
|
+
for mapping in self.reference_mappings(target=target):
|
|
350
|
+
if mapping.status not in allowed or (require_sequence and not mapping.sequence_validated):
|
|
351
|
+
continue
|
|
352
|
+
for observation in self.measurements(target=mapping.source, **observation_filters):
|
|
353
|
+
yield MappedMeasurement(observation, mapping, target)
|
|
354
|
+
|
|
270
355
|
def pairings(self, *, reference=None):
|
|
271
356
|
"""Iterate source-declared pairings; legacy releases have none."""
|
|
272
357
|
if self._storage_version == 1:
|
|
@@ -275,7 +360,7 @@ class QuantitativeDataset:
|
|
|
275
360
|
return self._records("assay_pair", AssayPair, reference=reference)
|
|
276
361
|
|
|
277
362
|
def relations(self, *, reference=None):
|
|
278
|
-
"""Iterate
|
|
363
|
+
"""Iterate same-species relationships, optionally scoped by their left reference."""
|
|
279
364
|
if self._storage_version == 1:
|
|
280
365
|
self._scope(reference)
|
|
281
366
|
return iter(())
|
|
@@ -307,14 +392,16 @@ class QuantitativeDataset:
|
|
|
307
392
|
if pair_ids is not None and pair.pair_id not in pair_ids:
|
|
308
393
|
continue
|
|
309
394
|
by_target = []
|
|
310
|
-
for assay in (pair.
|
|
395
|
+
for assay_reference, assay in ((pair.reference, pair.left_assay_id),
|
|
396
|
+
(pair.right_reference, pair.right_assay_id)):
|
|
311
397
|
values = {}
|
|
312
398
|
if assay is not None:
|
|
313
|
-
for value in self.measurements(reference=
|
|
399
|
+
for value in self.measurements(reference=assay_reference, assay_id=assay):
|
|
314
400
|
values.setdefault(value.target.key, []).append(value)
|
|
315
401
|
by_target.append(values)
|
|
316
402
|
for relation in relations:
|
|
317
|
-
if relation.left.reference.key == pair.reference.key
|
|
403
|
+
if (relation.left.reference.key == pair.reference.key
|
|
404
|
+
and relation.right.reference.key == pair.right_reference.key):
|
|
318
405
|
yield MatchedMeasurements(pair, relation,
|
|
319
406
|
tuple(by_target[0].get(relation.left.key, ())),
|
|
320
407
|
tuple(by_target[1].get(relation.right.key, ())))
|
|
@@ -353,6 +440,9 @@ class QuantitativeDataset:
|
|
|
353
440
|
for row in connection.execute(query, [value for _, value in selected]):
|
|
354
441
|
values = json.loads(row[0])
|
|
355
442
|
values["target"] = ScopedTarget(self._references[row[1]], row[2], row[4], row[3])
|
|
443
|
+
if "input_references" in values:
|
|
444
|
+
values["input_references"] = tuple(MeasurementReference.from_dict(value)
|
|
445
|
+
for value in values["input_references"])
|
|
356
446
|
yield Measurement(**values)
|
|
357
447
|
|
|
358
448
|
@property
|
|
@@ -36,6 +36,12 @@ 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
|
+
subparser.add_argument("--mapped", action="store_true",
|
|
40
|
+
help="Return source observations with explicit reference mappings")
|
|
41
|
+
subparser.add_argument("--mapping-status", action="append",
|
|
42
|
+
help="Mapping status to include; default unique")
|
|
43
|
+
subparser.add_argument("--require-sequence", action="store_true",
|
|
44
|
+
help="Require recorded exact sequence validation for mapped queries")
|
|
39
45
|
if command == "pairs":
|
|
40
46
|
subparser.add_argument("--basis", action="append", required=True,
|
|
41
47
|
help="Allowed pairing basis; repeat for multiple bases")
|
|
@@ -84,10 +90,19 @@ def run(argv=None, prog=None):
|
|
|
84
90
|
parser.error("Target queries require --target, --kind, --namespace and --reference")
|
|
85
91
|
references = {r.key: r for r in dataset.manifest.references}
|
|
86
92
|
target = ScopedTarget(references[args.reference], args.kind, args.target, args.namespace)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
filters = dict(sample_id=args.sample, assay_id=args.assay_id,
|
|
94
|
+
quantity=args.quantity, status=args.status)
|
|
95
|
+
if args.mapped:
|
|
96
|
+
if target is None:
|
|
97
|
+
parser.error("--mapped requires an exact target and reference")
|
|
98
|
+
measurements = dataset.mapped_measurements(
|
|
99
|
+
target, mapping_statuses=args.mapping_status or ["unique"],
|
|
100
|
+
require_sequence=args.require_sequence, **filters)
|
|
101
|
+
else:
|
|
102
|
+
if args.mapping_status or args.require_sequence:
|
|
103
|
+
parser.error("Mapping filters require --mapped")
|
|
104
|
+
measurements = dataset.measurements(target=target, reference=args.reference, **filters)
|
|
105
|
+
for measurement in measurements:
|
|
91
106
|
sys.stdout.write(json.dumps(measurement.to_dict()) + "\n")
|
|
92
107
|
return
|
|
93
108
|
json.dump(result, sys.stdout, indent=2)
|
|
@@ -91,6 +91,9 @@ class _Record:
|
|
|
91
91
|
row["reference"] = ReferenceContext(**row["reference"])
|
|
92
92
|
targets.append(ScopedTarget(**row))
|
|
93
93
|
values["targets"] = tuple(targets)
|
|
94
|
+
if "input_references" in values:
|
|
95
|
+
values["input_references"] = tuple(MeasurementReference.from_dict(row)
|
|
96
|
+
for row in values["input_references"])
|
|
94
97
|
return cls(**values)
|
|
95
98
|
|
|
96
99
|
|
|
@@ -246,6 +249,33 @@ class Assay(_Record):
|
|
|
246
249
|
_metadata(self, "normalization", "calibration", "attributes", "provenance")
|
|
247
250
|
|
|
248
251
|
|
|
252
|
+
@dataclass(frozen=True)
|
|
253
|
+
class MeasurementReference(_Record):
|
|
254
|
+
"""An input observation identified across dataset releases and references."""
|
|
255
|
+
|
|
256
|
+
dataset_id: str
|
|
257
|
+
dataset_version: str
|
|
258
|
+
reference: ReferenceContext
|
|
259
|
+
measurement_id: str
|
|
260
|
+
|
|
261
|
+
def __post_init__(self):
|
|
262
|
+
for name in ("dataset_id", "dataset_version", "measurement_id"):
|
|
263
|
+
_identifier(getattr(self, name), name)
|
|
264
|
+
if not isinstance(self.reference, ReferenceContext):
|
|
265
|
+
raise TypeError("Measurement reference requires a ReferenceContext")
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def key(self):
|
|
269
|
+
return self.dataset_id, self.dataset_version, self.reference.key, self.measurement_id
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def from_measurement(cls, measurement):
|
|
273
|
+
if not isinstance(measurement, Measurement):
|
|
274
|
+
raise TypeError("Expected a Measurement")
|
|
275
|
+
return cls(measurement.dataset_id, measurement.dataset_version,
|
|
276
|
+
measurement.target.reference, measurement.measurement_id)
|
|
277
|
+
|
|
278
|
+
|
|
249
279
|
@dataclass(frozen=True)
|
|
250
280
|
class Measurement(_Record):
|
|
251
281
|
"""A native value with units, missingness, target resolution and provenance.
|
|
@@ -275,6 +305,7 @@ class Measurement(_Record):
|
|
|
275
305
|
input_measurements: tuple = ()
|
|
276
306
|
derivation: Mapping = field(default_factory=dict)
|
|
277
307
|
provenance: Mapping = field(default_factory=dict)
|
|
308
|
+
input_references: tuple = ()
|
|
278
309
|
|
|
279
310
|
def __post_init__(self):
|
|
280
311
|
for name in ("measurement_id", "sample_id", "assay_id", "unit", "dataset_id", "dataset_version"):
|
|
@@ -314,11 +345,26 @@ class Measurement(_Record):
|
|
|
314
345
|
if len(set(inputs)) != len(inputs) or self.measurement_id in inputs:
|
|
315
346
|
raise ValueError("Derived input IDs must be unique and exclude the output")
|
|
316
347
|
object.__setattr__(self, "input_measurements", inputs)
|
|
348
|
+
references = tuple(self.input_references)
|
|
349
|
+
if inputs and references:
|
|
350
|
+
raise ValueError("Choose legacy same-context input IDs or explicit input references, not both")
|
|
351
|
+
if any(not isinstance(value, MeasurementReference) for value in references):
|
|
352
|
+
raise TypeError("input_references must contain MeasurementReferences")
|
|
353
|
+
own_key = (self.dataset_id, self.dataset_version, self.target.reference.key, self.measurement_id)
|
|
354
|
+
keys = {value.key for value in references}
|
|
355
|
+
if len(keys) != len(references) or own_key in keys:
|
|
356
|
+
raise ValueError("Input references must be unique and exclude the output")
|
|
357
|
+
for value in references:
|
|
358
|
+
if (value.dataset_id, value.dataset_version) != (self.dataset_id, self.dataset_version):
|
|
359
|
+
raise ValueError("Derived inputs must belong to the same dataset release")
|
|
360
|
+
if value.reference.taxon_id != self.target.reference.taxon_id:
|
|
361
|
+
raise ValueError("Derived abundance inputs cannot cross species")
|
|
362
|
+
object.__setattr__(self, "input_references", references)
|
|
317
363
|
if self.origin in ("derived", "predicted") and not self.derivation:
|
|
318
364
|
raise ValueError("Derived/predicted values require a named method and assumptions")
|
|
319
365
|
if self.origin in ("derived", "predicted") and isinstance(self.derivation, Mapping):
|
|
320
366
|
_identifier(self.derivation.get("method"), "derivation method")
|
|
321
|
-
if self.origin == "observed" and (inputs or self.derivation):
|
|
367
|
+
if self.origin == "observed" and (inputs or references or self.derivation):
|
|
322
368
|
raise ValueError("An observed value cannot contain a derivation")
|
|
323
369
|
_metadata(self, "derivation", "provenance")
|
|
324
370
|
|