orfdata 0.3.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.3.0/orfdata.egg-info → orfdata-0.5.0}/PKG-INFO +1 -1
- {orfdata-0.3.0 → orfdata-0.5.0}/docs/measurements.md +11 -4
- orfdata-0.5.0/docs/pairing.md +97 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/docs/reference.md +2 -3
- orfdata-0.5.0/docs/source-references.md +90 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/__init__.py +27 -1
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/dataset.py +218 -14
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/dataset_cli.py +36 -5
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/measurements.py +48 -1
- orfdata-0.5.0/orfdata/pairing.py +362 -0
- {orfdata-0.3.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.3.0 → orfdata-0.5.0/orfdata.egg-info}/PKG-INFO +1 -1
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata.egg-info/SOURCES.txt +9 -0
- orfdata-0.5.0/tests/conftest.py +45 -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.3.0 → orfdata-0.5.0}/tests/test_measurements.py +2 -33
- orfdata-0.5.0/tests/test_pairing.py +269 -0
- orfdata-0.5.0/tests/test_reference_mapping.py +225 -0
- orfdata-0.3.0/orfdata/version.py +0 -1
- {orfdata-0.3.0 → orfdata-0.5.0}/LICENSE +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/MANIFEST.in +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/README.md +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/deploy.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/docs/design.md +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/docs/species.md +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/lint.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/build.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/catalog.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/cli.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/datasets.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/orf-evidence/manifest.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/datasets.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/initiation_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/model.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata/sequence.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata.egg-info/dependency_links.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata.egg-info/entry_points.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata.egg-info/requires.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/orfdata.egg-info/top_level.txt +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/pyproject.toml +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/setup.cfg +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/test.sh +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/data/references/README.md +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/data/references/homo_sapiens.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/data/references/mus_musculus.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/test_initiation_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/test_reference_context.py +0 -0
- {orfdata-0.3.0 → orfdata-0.5.0}/tests/test_reference_orf.py +0 -0
- {orfdata-0.3.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
|
|
@@ -80,9 +84,9 @@ records technical replicates, runs, fractions, TMT plex/channel/bridge,
|
|
|
80
84
|
normalization and calibration. A sample's strain/breed belongs in its attributes;
|
|
81
85
|
it need not be the strain used to assemble its reference genome. `TargetMapping`
|
|
82
86
|
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
|
-
|
|
87
|
+
representative does not become a uniquely measured protein. See
|
|
88
|
+
[pairing and orthology comparisons](pairing.md) for explicit experimental
|
|
89
|
+
relationships and derived RNA/protein ratios.
|
|
86
90
|
|
|
87
91
|
## Importing a source
|
|
88
92
|
|
|
@@ -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.
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
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
|
+
|
|
35
|
+
The CLI streams the same table rows:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
orfdata datasets pairs DATASET --version RELEASE --basis same_material
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Which targets are being compared?
|
|
42
|
+
|
|
43
|
+
`TargetRelation` links exact, reference-scoped targets with mapping evidence
|
|
44
|
+
and cardinality. `same_target` compares one target across assays/samples.
|
|
45
|
+
`canonical_translation` links a gene/transcript to its annotated protein.
|
|
46
|
+
`parent_rna_context` links parent RNA to an ORF but keeps the RNA value attached
|
|
47
|
+
to its original gene/transcript. `shared_group_member` records an ambiguous
|
|
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.
|
|
51
|
+
|
|
52
|
+
Importers pass `AssayPair` and `TargetRelation` iterators as `pairings=` and
|
|
53
|
+
`relations=` to `QuantitativeDataset.create`. IDs belong to the dataset release
|
|
54
|
+
and left reference context. The right assay/target keeps its own context.
|
|
55
|
+
Invalid assay references, contradictory known donor
|
|
56
|
+
identities and conflicting one-to-one assignments fail the import. Original
|
|
57
|
+
storage-version-1 datasets remain readable and have no declared relationships.
|
|
58
|
+
|
|
59
|
+
## Ratios are derived measurements
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
ratio = match.ratio(
|
|
63
|
+
selected_protein_measurement, selected_rna_measurement,
|
|
64
|
+
measurement_id="ratio-1", method="native protein/RNA ratio",
|
|
65
|
+
normalization={"protein": "source bridge normalization", "rna": "source TPM"},
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Select actual measurements from opposite sides of the match. Ratios require
|
|
70
|
+
one-to-one target attribution and linear inputs. Parent-RNA/ORF and shared-group
|
|
71
|
+
links cannot produce target-specific ratios. There is no implicit log conversion,
|
|
72
|
+
unit conversion, replicate averaging or uncertainty propagation.
|
|
73
|
+
|
|
74
|
+
An iBAQ/TPM ratio keeps `(iBAQ)/(TPM)` units. Copies-per-cell values also retain
|
|
75
|
+
their native composite units and the calibration recorded on their source
|
|
76
|
+
assays. Missing, censored or nonpositive denominators produce an unresolved
|
|
77
|
+
`not_measured` result with its reason, never infinity or a measured zero. RPF/RNA
|
|
78
|
+
ratios are source-defined translation-efficiency measures, not measured flux.
|
|
79
|
+
All derived values retain their input IDs, pairing, target attribution and
|
|
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.
|
|
84
|
+
|
|
85
|
+
## Across species
|
|
86
|
+
|
|
87
|
+
`compare_orthologs(left_dataset, right_dataset, mappings)` is separate and
|
|
88
|
+
opt-in. Each `OrthologyMapping` pins its source file, release, both reference
|
|
89
|
+
contexts, cardinality, confidence definition and evidence. The default accepts
|
|
90
|
+
one-to-one canonical gene mappings; one-to-many relationships require explicit
|
|
91
|
+
permission through `allowed_cardinalities`.
|
|
92
|
+
|
|
93
|
+
The result contains each species' original observations. It neither merges
|
|
94
|
+
identities nor transfers abundance or protein-existence evidence. ORF comparisons
|
|
95
|
+
additionally require their own sequence, frame and positional evidence and
|
|
96
|
+
`allow_orfs=True`; a gene ortholog cannot establish an orthologous ncORF.
|
|
97
|
+
Human, mouse, dog, yeast and arbitrary other taxa use this same API.
|
|
@@ -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,9 +15,26 @@ 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
|
|
28
|
+
from .pairing import (
|
|
29
|
+
AssayPair,
|
|
30
|
+
MatchedMeasurements,
|
|
31
|
+
OrthologyComparison,
|
|
32
|
+
OrthologyMapping,
|
|
33
|
+
TargetRelation,
|
|
34
|
+
compare_orthologs,
|
|
35
|
+
)
|
|
20
36
|
from .reference import ReferenceContext, ScopedTarget
|
|
37
|
+
from .reference_mapping import MappedMeasurement, ReferenceMapping
|
|
21
38
|
from .version import __version__
|
|
22
39
|
|
|
23
40
|
__all__ = [
|
|
@@ -39,7 +56,16 @@ __all__ = [
|
|
|
39
56
|
"Sample",
|
|
40
57
|
"Assay",
|
|
41
58
|
"Measurement",
|
|
59
|
+
"MeasurementReference",
|
|
42
60
|
"TargetMapping",
|
|
61
|
+
"AssayPair",
|
|
62
|
+
"TargetRelation",
|
|
63
|
+
"MatchedMeasurements",
|
|
64
|
+
"OrthologyMapping",
|
|
65
|
+
"OrthologyComparison",
|
|
66
|
+
"compare_orthologs",
|
|
67
|
+
"ReferenceMapping",
|
|
68
|
+
"MappedMeasurement",
|
|
43
69
|
"load_catalog",
|
|
44
70
|
"UORFCatalog",
|
|
45
71
|
"UORFRecord",
|
|
@@ -13,10 +13,20 @@ from .measurements import (
|
|
|
13
13
|
Assay,
|
|
14
14
|
DatasetManifest,
|
|
15
15
|
Measurement,
|
|
16
|
+
MeasurementReference,
|
|
16
17
|
Sample,
|
|
17
18
|
TargetMapping,
|
|
18
19
|
)
|
|
20
|
+
from .pairing import (
|
|
21
|
+
PAIRING_BASES,
|
|
22
|
+
TEMPORAL_RELATIONS,
|
|
23
|
+
AssayPair,
|
|
24
|
+
MatchedMeasurements,
|
|
25
|
+
TargetRelation,
|
|
26
|
+
_check_one_to_one,
|
|
27
|
+
)
|
|
19
28
|
from .reference import ReferenceContext, ScopedTarget
|
|
29
|
+
from .reference_mapping import MappedMeasurement, ReferenceMapping
|
|
20
30
|
|
|
21
31
|
_SCHEMA = """
|
|
22
32
|
CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
@@ -44,13 +54,41 @@ CREATE TABLE measurement (
|
|
|
44
54
|
FOREIGN KEY(reference_key, assay_id, sample_id)
|
|
45
55
|
REFERENCES assay(reference_key, id, sample_id));
|
|
46
56
|
CREATE TABLE input_measurement (
|
|
47
|
-
reference_key TEXT NOT NULL, output_id TEXT NOT NULL,
|
|
48
|
-
|
|
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),
|
|
49
60
|
FOREIGN KEY(reference_key, output_id) REFERENCES measurement(reference_key, id),
|
|
50
|
-
FOREIGN KEY(
|
|
61
|
+
FOREIGN KEY(input_reference_key, input_id) REFERENCES measurement(reference_key, id));
|
|
51
62
|
CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
|
|
52
63
|
CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
|
|
53
64
|
CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
|
|
65
|
+
CREATE TABLE assay_pair (
|
|
66
|
+
reference_key TEXT NOT NULL REFERENCES reference(key), id TEXT NOT NULL,
|
|
67
|
+
right_reference_key TEXT NOT NULL REFERENCES reference(key),
|
|
68
|
+
left_assay_id TEXT, right_assay_id TEXT, record TEXT NOT NULL,
|
|
69
|
+
PRIMARY KEY(reference_key, id),
|
|
70
|
+
FOREIGN KEY(reference_key, left_assay_id) REFERENCES assay(reference_key, id),
|
|
71
|
+
FOREIGN KEY(right_reference_key, right_assay_id) REFERENCES assay(reference_key, id));
|
|
72
|
+
CREATE TABLE target_relation (
|
|
73
|
+
reference_key TEXT NOT NULL, id TEXT NOT NULL,
|
|
74
|
+
right_reference_key TEXT NOT NULL,
|
|
75
|
+
left_target_key TEXT NOT NULL, right_target_key TEXT NOT NULL, record TEXT NOT NULL,
|
|
76
|
+
PRIMARY KEY(reference_key, id),
|
|
77
|
+
FOREIGN KEY(reference_key, left_target_key) REFERENCES target(reference_key, 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);
|
|
54
92
|
"""
|
|
55
93
|
|
|
56
94
|
|
|
@@ -73,7 +111,8 @@ class QuantitativeDataset:
|
|
|
73
111
|
def __init__(self, path):
|
|
74
112
|
self.path = Path(path).resolve()
|
|
75
113
|
with closing(self._connect()) as connection:
|
|
76
|
-
|
|
114
|
+
self._storage_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
|
115
|
+
if self._storage_version not in (1, 2, 3):
|
|
77
116
|
raise ValueError("Unsupported quantitative database schema")
|
|
78
117
|
row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
|
|
79
118
|
if row is None:
|
|
@@ -88,7 +127,8 @@ class QuantitativeDataset:
|
|
|
88
127
|
return connection
|
|
89
128
|
|
|
90
129
|
@classmethod
|
|
91
|
-
def create(cls, path, manifest, *, samples, assays, measurements, mappings=()
|
|
130
|
+
def create(cls, path, manifest, *, samples, assays, measurements, mappings=(),
|
|
131
|
+
pairings=(), relations=(), reference_mappings=()):
|
|
92
132
|
"""Write a new dataset from iterators, validating scope and foreign keys.
|
|
93
133
|
|
|
94
134
|
Never overwrite an existing file. An interrupted/invalid build removes
|
|
@@ -106,7 +146,7 @@ class QuantitativeDataset:
|
|
|
106
146
|
connection = sqlite3.connect(path)
|
|
107
147
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
108
148
|
connection.executescript(_SCHEMA)
|
|
109
|
-
connection.execute("PRAGMA user_version=
|
|
149
|
+
connection.execute("PRAGMA user_version=3")
|
|
110
150
|
connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
|
|
111
151
|
(_json(manifest.to_dict()),))
|
|
112
152
|
for reference in manifest.references:
|
|
@@ -125,12 +165,68 @@ class QuantitativeDataset:
|
|
|
125
165
|
connection.execute("INSERT INTO assay VALUES (?, ?, ?, ?)",
|
|
126
166
|
(assay.reference.key, assay.assay_id, assay.sample_id,
|
|
127
167
|
_json(assay.to_dict())))
|
|
168
|
+
for pair in pairings:
|
|
169
|
+
if not isinstance(pair, AssayPair):
|
|
170
|
+
raise TypeError("pairings must contain AssayPairs")
|
|
171
|
+
if (pair.dataset_id, pair.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
172
|
+
raise ValueError("Pairing belongs to a different dataset release")
|
|
173
|
+
if pair.basis in ("same_material", "adjacent_donor_tissue"):
|
|
174
|
+
units = []
|
|
175
|
+
for reference, identifier in ((pair.reference, pair.left_assay_id),
|
|
176
|
+
(pair.right_reference, pair.right_assay_id)):
|
|
177
|
+
row = connection.execute(
|
|
178
|
+
"SELECT s.record FROM assay a JOIN sample s ON a.reference_key=s.reference_key "
|
|
179
|
+
"AND a.sample_id=s.id WHERE a.reference_key=? AND a.id=?",
|
|
180
|
+
(reference.key, identifier)).fetchone()
|
|
181
|
+
if row:
|
|
182
|
+
unit = json.loads(row[0]).get("biological_unit_id")
|
|
183
|
+
if unit is not None:
|
|
184
|
+
units.append(unit)
|
|
185
|
+
if len(set(units)) > 1:
|
|
186
|
+
raise ValueError("Declared pairing contradicts biological-unit identities")
|
|
187
|
+
connection.execute("INSERT INTO assay_pair VALUES (?, ?, ?, ?, ?, ?)",
|
|
188
|
+
(pair.reference.key, pair.pair_id, pair.right_reference.key, pair.left_assay_id,
|
|
189
|
+
pair.right_assay_id, _json(pair.to_dict())))
|
|
190
|
+
relation_links = ({}, {})
|
|
191
|
+
for relation in relations:
|
|
192
|
+
if not isinstance(relation, TargetRelation):
|
|
193
|
+
raise TypeError("relations must contain TargetRelations")
|
|
194
|
+
if (relation.dataset_id, relation.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
195
|
+
raise ValueError("Target relation belongs to a different dataset release")
|
|
196
|
+
for target in (relation.left, relation.right):
|
|
197
|
+
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
198
|
+
(target.reference.key, _target_key(target), target.kind,
|
|
199
|
+
target.namespace, target.identifier))
|
|
200
|
+
for index, (first, second) in enumerate(((relation.left, relation.right), (relation.right, relation.left))):
|
|
201
|
+
_check_one_to_one(relation_links[index],
|
|
202
|
+
(relation.relation, first.key, second.reference.key, second.kind, second.namespace), second.key,
|
|
203
|
+
relation.cardinality == "one_to_one")
|
|
204
|
+
connection.execute("INSERT INTO target_relation VALUES (?, ?, ?, ?, ?, ?)",
|
|
205
|
+
(relation.left.reference.key, relation.relation_id,
|
|
206
|
+
relation.right.reference.key,
|
|
207
|
+
_target_key(relation.left), _target_key(relation.right),
|
|
208
|
+
_json(relation.to_dict())))
|
|
128
209
|
for mapping in mappings:
|
|
129
210
|
if not isinstance(mapping, TargetMapping):
|
|
130
211
|
raise TypeError("mappings must contain TargetMappings")
|
|
131
212
|
connection.execute("INSERT INTO mapping VALUES (?, ?, ?)",
|
|
132
213
|
(mapping.reference.key, mapping.mapping_id,
|
|
133
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))
|
|
134
230
|
source_ids = {s.asset_id for s in manifest.sources}
|
|
135
231
|
for observation in measurements:
|
|
136
232
|
if not isinstance(observation, Measurement):
|
|
@@ -140,15 +236,17 @@ class QuantitativeDataset:
|
|
|
140
236
|
source = observation.provenance.get("source_asset")
|
|
141
237
|
if source is not None and source not in source_ids:
|
|
142
238
|
raise ValueError("Measurement provenance names an unknown source asset")
|
|
143
|
-
if source is None and not observation.input_measurements:
|
|
239
|
+
if source is None and not (observation.input_measurements or observation.input_references):
|
|
144
240
|
raise ValueError("Measurement requires a source_asset or input measurements")
|
|
145
241
|
reference_key = observation.target.reference.key
|
|
146
|
-
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:
|
|
147
245
|
if connection.execute(
|
|
148
246
|
"SELECT 1 FROM measurement WHERE reference_key=? AND id=?",
|
|
149
|
-
(
|
|
247
|
+
(input_reference, identifier),
|
|
150
248
|
).fetchone() is None:
|
|
151
|
-
raise ValueError("Derived inputs must precede outputs in
|
|
249
|
+
raise ValueError("Derived inputs must precede outputs in their declared reference contexts")
|
|
152
250
|
target = observation.target
|
|
153
251
|
target_key = _target_key(target)
|
|
154
252
|
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
@@ -160,11 +258,12 @@ class QuantitativeDataset:
|
|
|
160
258
|
(reference_key, observation.measurement_id, target_key,
|
|
161
259
|
observation.sample_id, observation.assay_id,
|
|
162
260
|
observation.quantity, observation.status, _json(record)))
|
|
163
|
-
connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?)",
|
|
164
|
-
((reference_key, observation.measurement_id, identifier)
|
|
165
|
-
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))
|
|
166
264
|
counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
|
|
167
|
-
for table in ("sample", "assay", "mapping", "target", "measurement"
|
|
265
|
+
for table in ("sample", "assay", "mapping", "target", "measurement",
|
|
266
|
+
"assay_pair", "target_relation", "reference_mapping")}
|
|
168
267
|
connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
|
|
169
268
|
connection.commit()
|
|
170
269
|
connection.close()
|
|
@@ -205,6 +304,108 @@ class QuantitativeDataset:
|
|
|
205
304
|
def mappings(self, *, reference=None):
|
|
206
305
|
return self._records("mapping", TargetMapping, reference=reference)
|
|
207
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
|
+
|
|
355
|
+
def pairings(self, *, reference=None):
|
|
356
|
+
"""Iterate source-declared pairings; legacy releases have none."""
|
|
357
|
+
if self._storage_version == 1:
|
|
358
|
+
self._scope(reference)
|
|
359
|
+
return iter(())
|
|
360
|
+
return self._records("assay_pair", AssayPair, reference=reference)
|
|
361
|
+
|
|
362
|
+
def relations(self, *, reference=None):
|
|
363
|
+
"""Iterate same-species relationships, optionally scoped by their left reference."""
|
|
364
|
+
if self._storage_version == 1:
|
|
365
|
+
self._scope(reference)
|
|
366
|
+
return iter(())
|
|
367
|
+
return self._records("target_relation", TargetRelation, reference=reference)
|
|
368
|
+
|
|
369
|
+
def matched_measurements(self, *, allowed_bases, allowed_temporal=("concurrent",),
|
|
370
|
+
reference=None, pair_ids=None, relation_ids=None):
|
|
371
|
+
"""Iterate explicit assay/target matches, preserving all values and missing mates.
|
|
372
|
+
|
|
373
|
+
Defaults require concurrent assays; opting into longitudinal or unknown
|
|
374
|
+
timing is explicit. No matching by tissue names or inferred orthology.
|
|
375
|
+
Each side contains its own original observations, never a Cartesian
|
|
376
|
+
multiplication or an aggregate over technical replicates/quantities.
|
|
377
|
+
"""
|
|
378
|
+
bases, temporal = set(allowed_bases), set(allowed_temporal)
|
|
379
|
+
if isinstance(pair_ids, str) or isinstance(relation_ids, str):
|
|
380
|
+
raise TypeError("Relationship ID selections must be sequences, not a single string")
|
|
381
|
+
if not bases or bases - set(PAIRING_BASES):
|
|
382
|
+
raise ValueError("Specify valid allowed pairing bases explicitly")
|
|
383
|
+
if not temporal or temporal - set(TEMPORAL_RELATIONS):
|
|
384
|
+
raise ValueError("Specify valid allowed temporal relationships")
|
|
385
|
+
if (pair_ids or relation_ids) and reference is None and len(self._references) > 1:
|
|
386
|
+
raise ValueError("Relationship ID queries across species require a reference")
|
|
387
|
+
relations = tuple(relation for relation in self.relations(reference=reference)
|
|
388
|
+
if relation_ids is None or relation.relation_id in relation_ids)
|
|
389
|
+
for pair in self.pairings(reference=reference):
|
|
390
|
+
if pair.basis not in bases or pair.temporal_relation not in temporal:
|
|
391
|
+
continue
|
|
392
|
+
if pair_ids is not None and pair.pair_id not in pair_ids:
|
|
393
|
+
continue
|
|
394
|
+
by_target = []
|
|
395
|
+
for assay_reference, assay in ((pair.reference, pair.left_assay_id),
|
|
396
|
+
(pair.right_reference, pair.right_assay_id)):
|
|
397
|
+
values = {}
|
|
398
|
+
if assay is not None:
|
|
399
|
+
for value in self.measurements(reference=assay_reference, assay_id=assay):
|
|
400
|
+
values.setdefault(value.target.key, []).append(value)
|
|
401
|
+
by_target.append(values)
|
|
402
|
+
for relation in relations:
|
|
403
|
+
if (relation.left.reference.key == pair.reference.key
|
|
404
|
+
and relation.right.reference.key == pair.right_reference.key):
|
|
405
|
+
yield MatchedMeasurements(pair, relation,
|
|
406
|
+
tuple(by_target[0].get(relation.left.key, ())),
|
|
407
|
+
tuple(by_target[1].get(relation.right.key, ())))
|
|
408
|
+
|
|
208
409
|
def measurements(self, *, target=None, reference=None, sample_id=None,
|
|
209
410
|
assay_id=None, quantity=None, status=None):
|
|
210
411
|
"""Iterate native observations; a target match is exact, never orthology.
|
|
@@ -239,6 +440,9 @@ class QuantitativeDataset:
|
|
|
239
440
|
for row in connection.execute(query, [value for _, value in selected]):
|
|
240
441
|
values = json.loads(row[0])
|
|
241
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"])
|
|
242
446
|
yield Measurement(**values)
|
|
243
447
|
|
|
244
448
|
@property
|