orfdata 0.7.0__tar.gz → 0.9.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.
Files changed (109) hide show
  1. {orfdata-0.7.0 → orfdata-0.9.0}/MANIFEST.in +1 -0
  2. {orfdata-0.7.0/orfdata.egg-info → orfdata-0.9.0}/PKG-INFO +1 -1
  3. {orfdata-0.7.0 → orfdata-0.9.0}/docs/measurements.md +20 -1
  4. {orfdata-0.7.0 → orfdata-0.9.0}/docs/pfizer-protein.md +5 -2
  5. orfdata-0.9.0/docs/storage.md +45 -0
  6. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/dataset.py +157 -59
  7. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/dataset_cli.py +5 -1
  8. orfdata-0.9.0/orfdata/version.py +1 -0
  9. {orfdata-0.7.0 → orfdata-0.9.0/orfdata.egg-info}/PKG-INFO +1 -1
  10. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata.egg-info/SOURCES.txt +12 -1
  11. orfdata-0.9.0/tests/data/storage/README.md +15 -0
  12. orfdata-0.9.0/tests/data/storage/generate.py +74 -0
  13. orfdata-0.9.0/tests/data/storage/v1.json +582 -0
  14. orfdata-0.9.0/tests/data/storage/v1.sqlite +0 -0
  15. orfdata-0.9.0/tests/data/storage/v2.json +584 -0
  16. orfdata-0.9.0/tests/data/storage/v2.sqlite +0 -0
  17. orfdata-0.9.0/tests/data/storage/v3.json +613 -0
  18. orfdata-0.9.0/tests/data/storage/v3.sqlite +0 -0
  19. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_pairing.py +4 -8
  20. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_reference_mapping.py +2 -6
  21. orfdata-0.9.0/tests/test_selectors.py +130 -0
  22. orfdata-0.9.0/tests/test_storage.py +76 -0
  23. orfdata-0.7.0/orfdata/version.py +0 -1
  24. {orfdata-0.7.0 → orfdata-0.9.0}/LICENSE +0 -0
  25. {orfdata-0.7.0 → orfdata-0.9.0}/README.md +0 -0
  26. {orfdata-0.7.0 → orfdata-0.9.0}/deploy.sh +0 -0
  27. {orfdata-0.7.0 → orfdata-0.9.0}/docs/copy-yeast.md +0 -0
  28. {orfdata-0.7.0 → orfdata-0.9.0}/docs/design.md +0 -0
  29. {orfdata-0.7.0 → orfdata-0.9.0}/docs/pairing.md +0 -0
  30. {orfdata-0.7.0 → orfdata-0.9.0}/docs/reference.md +0 -0
  31. {orfdata-0.7.0 → orfdata-0.9.0}/docs/source-references.md +0 -0
  32. {orfdata-0.7.0 → orfdata-0.9.0}/docs/species.md +0 -0
  33. {orfdata-0.7.0 → orfdata-0.9.0}/lint.sh +0 -0
  34. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/__init__.py +0 -0
  35. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/build.py +0 -0
  36. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/catalog.py +0 -0
  37. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/cli.py +0 -0
  38. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/datasets.json +0 -0
  39. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
  40. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
  41. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
  42. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/orf-evidence/manifest.json +0 -0
  43. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
  44. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
  45. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
  46. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
  47. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/datasets.py +0 -0
  48. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/initiation_context.py +0 -0
  49. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/measurements.py +0 -0
  50. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/model.py +0 -0
  51. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/pairing.py +0 -0
  52. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/reference.py +0 -0
  53. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/reference_mapping.py +0 -0
  54. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/sequence.py +0 -0
  55. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/sources/__init__.py +0 -0
  56. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/sources/copy.py +0 -0
  57. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata/sources/pfizer.py +0 -0
  58. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata.egg-info/dependency_links.txt +0 -0
  59. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata.egg-info/entry_points.txt +0 -0
  60. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata.egg-info/requires.txt +0 -0
  61. {orfdata-0.7.0 → orfdata-0.9.0}/orfdata.egg-info/top_level.txt +0 -0
  62. {orfdata-0.7.0 → orfdata-0.9.0}/pyproject.toml +0 -0
  63. {orfdata-0.7.0 → orfdata-0.9.0}/setup.cfg +0 -0
  64. {orfdata-0.7.0 → orfdata-0.9.0}/test.sh +0 -0
  65. {orfdata-0.7.0 → orfdata-0.9.0}/tests/conftest.py +0 -0
  66. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905474.soft +0 -0
  67. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905474_KL9_fpkm.txt.gz +0 -0
  68. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905476.soft +0 -0
  69. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905476_KL10_fpkm.txt.gz +0 -0
  70. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905478.soft +0 -0
  71. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905478_KL11_fpkm.txt.gz +0 -0
  72. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905479.soft +0 -0
  73. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/GSM1905479_KL12_fpkm.txt.gz +0 -0
  74. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/NOTICE.md +0 -0
  75. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/expected.json +0 -0
  76. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/geo-series.soft +0 -0
  77. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/manifest.json +0 -0
  78. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/methods.pdf +0 -0
  79. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/paper.xml +0 -0
  80. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/pmc-metadata.json +0 -0
  81. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/qconcat.fasta +0 -0
  82. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/copy/workbook.xlsx +0 -0
  83. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/NOTICE.md +0 -0
  84. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/dog/expected.json +0 -0
  85. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/dog/manifest.json +0 -0
  86. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/dog/protein.tsv +0 -0
  87. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/dog/search.fasta +0 -0
  88. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/human/expected.json +0 -0
  89. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/human/manifest.json +0 -0
  90. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/human/protein.tsv +0 -0
  91. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/human/search.fasta +0 -0
  92. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/mouse/expected.json +0 -0
  93. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/mouse/manifest.json +0 -0
  94. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/mouse/protein.tsv +0 -0
  95. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/pfizer/mouse/search.fasta +0 -0
  96. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/README.md +0 -0
  97. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
  98. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/context-keys-v0.4.json +0 -0
  99. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/homo_sapiens.json +0 -0
  100. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/mus_musculus.json +0 -0
  101. {orfdata-0.7.0 → orfdata-0.9.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
  102. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_copy.py +0 -0
  103. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_cross_reference_pairing.py +0 -0
  104. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_initiation_context.py +0 -0
  105. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_measurements.py +0 -0
  106. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_pfizer.py +0 -0
  107. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_reference_context.py +0 -0
  108. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_reference_orf.py +0 -0
  109. {orfdata-0.7.0 → orfdata-0.9.0}/tests/test_uorf_catalog.py +0 -0
@@ -2,3 +2,4 @@ include lint.sh test.sh deploy.sh
2
2
  recursive-include docs *.md
3
3
  recursive-include tests *.py *.json *.md *.tsv *.fasta
4
4
  recursive-include tests/data/copy *.xlsx *.gz *.soft *.xml *.pdf
5
+ recursive-include tests/data/storage *.sqlite
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orfdata
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Reference open reading frames and experimental translation evidence on annotated transcripts
5
5
  Author: OpenVax
6
6
  License-Expression: Apache-2.0
@@ -51,6 +51,24 @@ For native sources with unreported genomic metadata, use explicit
51
51
  [source references and mapping views](source-references.md) to retain the original
52
52
  observation alongside its mapping to a PyEnsembl target.
53
53
 
54
+ Choose a native series explicitly when a source supplies several units:
55
+
56
+ ```python
57
+ values = dataset.measurements(quantity="protein_abundance", unit="iBAQ", scale="linear")
58
+ dataset.export_parquet("ibaq.parquet", unit="iBAQ", scale="linear")
59
+ ```
60
+
61
+ `unit`, `scale` and `log_base` also work with `mapped_measurements` and existing
62
+ storage v1-v4 releases. Units are exact and case-sensitive. Selecting
63
+ `scale="log", log_base=2` returns values already on that scale; nothing is transformed.
64
+ An unavailable series returns no observations. Contradictory linear/log requests
65
+ raise an error. Omitting selectors preserves all native series.
66
+
67
+ For example, Pfizer iBAQ and rIBAQ are two series from the same source rows.
68
+ Selecting or exporting both does not create additional donors, spectra or
69
+ independent detections. Source row/sample/assay lineage stays attached to each
70
+ measurement.
71
+
54
72
  The CLI uses the same operations:
55
73
 
56
74
  ```sh
@@ -145,4 +163,5 @@ arbitrary other taxa use the same schema. The core API needs neither pandas nor
145
163
  PyArrow. Optional `orfdata[columnar]` enables `dataset.export_parquet(path)`;
146
164
  query columns accompany full measurement JSON, preserving exact values and
147
165
  provenance even when a floating-point analysis column cannot represent an
148
- integer exactly.
166
+ integer exactly. [Compact storage and compatibility](storage.md) describes
167
+ new SQLite v4 writes, legacy readers and actual-source size/query benchmarks.
@@ -20,7 +20,7 @@ include sex. Channels and technical runs do not count as independent donors.
20
20
  pip install 'orfdata[data]'
21
21
  orfdata datasets list --species dog
22
22
  orfdata datasets download pfizer-atlas-dog-protein
23
- orfdata datasets query pfizer-atlas-dog-protein --quantity protein_abundance
23
+ orfdata datasets query pfizer-atlas-dog-protein --quantity protein_abundance --unit iBAQ --scale linear
24
24
  ```
25
25
 
26
26
  ```python
@@ -30,10 +30,13 @@ registry = DatasetRegistry()
30
30
  dataset = registry.open("pfizer-atlas-dog-protein") # After explicit download.
31
31
  reference, = dataset.manifest.references
32
32
  group = ScopedTarget(reference, "protein_group", "A0A0A0MPB0", "MSV000099744:ProteinGroup")
33
- for value in dataset.measurements(target=group):
33
+ for value in dataset.measurements(target=group, unit="iBAQ", scale="linear"):
34
34
  print(value.sample_id, value.unit, value.value, value.status)
35
35
  ```
36
36
 
37
+ Omit the unit selector to return both native series. They retain the same source
38
+ row and sample identity; two units do not imply two independent measurements.
39
+
37
40
  Every target retains its species and the checksum of the deposited UniProt
38
41
  search FASTA. The original genomic assembly and UniProt release are unknown.
39
42
  They stay unknown; a future annotation mapping must be explicit and validated.
@@ -0,0 +1,45 @@
1
+ # Quantitative storage
2
+
3
+ ORFdata 0.8 writes compact SQLite storage version 4. `Measurement`,
4
+ `QuantitativeDataset`, the registry and CLI keep the same public records and
5
+ query semantics. Human, mouse, dog, yeast and arbitrary taxa retain separate
6
+ reference-scoped identities.
7
+
8
+ References, targets and assays are stored once and addressed by internal integer
9
+ keys. Measurements retain their values and metadata; repeated required fields
10
+ are reconstructed from indexed columns and the dataset manifest. Fields equal
11
+ to the fixed version-4 defaults are omitted from stored JSON and restored on read.
12
+ Zeros, nulls, censoring, uncertainty, nested provenance and derived inputs retain
13
+ their meaning. Foreign keys prevent cross-reference target/assay assignments.
14
+ Internal integer keys never appear as public biological identifiers.
15
+
16
+ Published storage versions 1, 2 and 3 remain readable. There is no automatic
17
+ migration or replacement of downloaded artifacts. A new source rebuild can use
18
+ a different storage layout and therefore a different file checksum while
19
+ returning the same records. Registry downloads always verify the exact published
20
+ artifact's hash; source builds retain their own DataCache receipts.
21
+
22
+ The implementation streams observations and bounds its target cache to 131,072
23
+ entries. SQLite index statistics are populated during creation so a narrow
24
+ target query can use the target index instead of scanning an entire reference.
25
+ Raw SQL table layouts are internal; use the Python/CLI query and export APIs.
26
+
27
+ ## Actual-source benchmark
28
+
29
+ The first 100,000 rows of the native Pfizer/GEO RNA table contain four dog
30
+ libraries and 30,941 genes, including measured zeros. Source gzip SHA-256:
31
+ `a22780f92ffd0a028e30fe3392d73d01f787eb5a62b2043e8d26940191cbac70`.
32
+ This is a storage benchmark slice, not a newly released experimental dataset.
33
+
34
+ | Measurement | Storage v3 | Storage v4 |
35
+ |---|---:|---:|
36
+ | Installed size | 121.6 MB | 38.6 MB |
37
+ | Gzip size | 13.4 MB | 7.5 MB |
38
+ | Build time | 8.90 s | 4.28 s |
39
+ | One-gene query (four results) | 135 ms | 1.5 ms |
40
+ | Peak build RSS | 39.8 MB | 52.1 MB |
41
+
42
+ All 100,000 reconstructed observations were compared field for field. Timings
43
+ are local measurements on Python 3.12, not cross-machine guarantees; the bounded
44
+ target cache trades some memory for repeated-target lookup speed. Legacy tests
45
+ also use actual databases written by ORFdata 0.3.0, 0.4.0 and 0.5.0.
@@ -5,6 +5,8 @@ import json
5
5
  import os
6
6
  import sqlite3
7
7
  from contextlib import closing
8
+ from dataclasses import MISSING, fields
9
+ from functools import lru_cache
8
10
  from pathlib import Path
9
11
 
10
12
  from .measurements import (
@@ -16,6 +18,9 @@ from .measurements import (
16
18
  MeasurementReference,
17
19
  Sample,
18
20
  TargetMapping,
21
+ _identifier,
22
+ _number,
23
+ _plain,
19
24
  )
20
25
  from .pairing import (
21
26
  PAIRING_BASES,
@@ -30,38 +35,50 @@ from .reference_mapping import MappedMeasurement, ReferenceMapping
30
35
 
31
36
  _SCHEMA = """
32
37
  CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL);
33
- CREATE TABLE reference (key TEXT PRIMARY KEY, record TEXT NOT NULL);
38
+ CREATE TABLE reference (i INTEGER PRIMARY KEY, key TEXT UNIQUE NOT NULL,
39
+ record TEXT NOT NULL, UNIQUE(key, i));
34
40
  CREATE TABLE target (
35
- reference_key TEXT NOT NULL REFERENCES reference(key),
41
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
42
+ reference_key TEXT NOT NULL,
36
43
  key TEXT NOT NULL, kind TEXT NOT NULL, namespace TEXT NOT NULL,
37
- identifier TEXT NOT NULL, PRIMARY KEY(reference_key, key));
44
+ identifier TEXT NOT NULL, UNIQUE(reference_key, key), UNIQUE(reference_i, i),
45
+ FOREIGN KEY(reference_key, reference_i) REFERENCES reference(key, i));
38
46
  CREATE TABLE sample (
39
47
  reference_key TEXT NOT NULL REFERENCES reference(key),
40
48
  id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
41
49
  CREATE TABLE assay (
50
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
42
51
  reference_key TEXT NOT NULL, id TEXT NOT NULL, sample_id TEXT NOT NULL,
43
- record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
52
+ record TEXT NOT NULL, UNIQUE(reference_key, id), UNIQUE(reference_i, i),
44
53
  UNIQUE(reference_key, id, sample_id),
54
+ FOREIGN KEY(reference_key, reference_i) REFERENCES reference(key, i),
45
55
  FOREIGN KEY(reference_key, sample_id) REFERENCES sample(reference_key, id));
46
56
  CREATE TABLE mapping (
47
57
  reference_key TEXT NOT NULL REFERENCES reference(key),
48
58
  id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
49
- CREATE TABLE measurement (
50
- reference_key TEXT NOT NULL, id TEXT NOT NULL, target_key TEXT NOT NULL,
51
- sample_id TEXT NOT NULL, assay_id TEXT NOT NULL, quantity TEXT NOT NULL,
52
- status TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
53
- FOREIGN KEY(reference_key, target_key) REFERENCES target(reference_key, key),
54
- FOREIGN KEY(reference_key, assay_id, sample_id)
55
- REFERENCES assay(reference_key, id, sample_id));
56
- CREATE TABLE input_measurement (
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),
60
- FOREIGN KEY(reference_key, output_id) REFERENCES measurement(reference_key, id),
61
- FOREIGN KEY(input_reference_key, input_id) REFERENCES measurement(reference_key, id));
59
+ CREATE TABLE observation (
60
+ i INTEGER PRIMARY KEY, reference_i INTEGER NOT NULL,
61
+ id TEXT NOT NULL, target_i INTEGER NOT NULL, assay_i INTEGER NOT NULL,
62
+ quantity TEXT NOT NULL, status TEXT NOT NULL, record TEXT NOT NULL,
63
+ UNIQUE(reference_i, id),
64
+ FOREIGN KEY(reference_i, target_i) REFERENCES target(reference_i, i),
65
+ FOREIGN KEY(reference_i, assay_i) REFERENCES assay(reference_i, i));
66
+ CREATE TABLE observation_input (
67
+ output_i INTEGER NOT NULL REFERENCES observation(i),
68
+ input_i INTEGER NOT NULL REFERENCES observation(i), PRIMARY KEY(output_i, input_i));
69
+ CREATE VIEW measurement AS
70
+ SELECT r.key reference_key, m.id, t.key target_key, a.sample_id,
71
+ a.id assay_id, m.quantity, m.status, m.record
72
+ FROM observation m JOIN reference r ON r.i=m.reference_i
73
+ JOIN target t ON t.i=m.target_i JOIN assay a ON a.i=m.assay_i;
74
+ CREATE VIEW input_measurement AS
75
+ SELECT r.key reference_key, o.id output_id, s.key input_reference_key, i.id input_id
76
+ FROM observation_input p JOIN observation o ON o.i=p.output_i
77
+ JOIN observation i ON i.i=p.input_i JOIN reference r ON r.i=o.reference_i
78
+ JOIN reference s ON s.i=i.reference_i;
62
79
  CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
63
- CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
64
- CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
80
+ CREATE INDEX observation_lookup ON observation(reference_i, target_i, assay_i);
81
+ CREATE INDEX assay_lookup ON observation(reference_i, assay_i, quantity, status);
65
82
  CREATE TABLE assay_pair (
66
83
  reference_key TEXT NOT NULL REFERENCES reference(key), id TEXT NOT NULL,
67
84
  right_reference_key TEXT NOT NULL REFERENCES reference(key),
@@ -100,6 +117,42 @@ def _target_key(target):
100
117
  return hashlib.sha256(_json(target.key).encode()).hexdigest()
101
118
 
102
119
 
120
+ _STORED_FIELDS = tuple(f for f in fields(Measurement) if f.name not in {
121
+ "measurement_id", "target", "sample_id", "assay_id", "quantity", "status",
122
+ "dataset_id", "dataset_version"})
123
+ _V4_DEFAULTS = {
124
+ "origin": "observed", "scale": "linear", "log_base": None,
125
+ "uncertainty": None, "uncertainty_type": None, "detection_limit": None,
126
+ "input_measurements": (), "input_references": (), "derivation": {}, "provenance": {},
127
+ }
128
+
129
+
130
+ def _measurement_record(observation):
131
+ # Schema defaults are fixed here, independent of future model defaults.
132
+ # Do not serialize the target
133
+ # and then discard it: its full reference is already stored once.
134
+ result = {}
135
+ for field in _STORED_FIELDS:
136
+ value = getattr(observation, field.name)
137
+ default = _V4_DEFAULTS.get(field.name, MISSING)
138
+ if default is MISSING or value != default:
139
+ result[field.name] = _plain(value)
140
+ return result
141
+
142
+
143
+ def _validate_native_selectors(unit=None, scale=None, log_base=None):
144
+ if unit is not None:
145
+ _identifier(unit, "measurement unit selector")
146
+ if scale is not None and scale not in ("linear", "log"):
147
+ raise ValueError("Measurement scale selector must be linear or log")
148
+ if log_base is not None:
149
+ _number(log_base, "logarithm base selector")
150
+ if log_base <= 0 or log_base == 1:
151
+ raise ValueError("Logarithm base selector must be positive and not one")
152
+ if scale == "linear":
153
+ raise ValueError("A linear scale selector cannot have a logarithm base")
154
+
155
+
103
156
  class QuantitativeDataset:
104
157
  """A read-only release. Constructing or querying it never downloads data.
105
158
 
@@ -112,7 +165,7 @@ class QuantitativeDataset:
112
165
  self.path = Path(path).resolve()
113
166
  with closing(self._connect()) as connection:
114
167
  self._storage_version = connection.execute("PRAGMA user_version").fetchone()[0]
115
- if self._storage_version not in (1, 2, 3):
168
+ if self._storage_version not in (1, 2, 3, 4):
116
169
  raise ValueError("Unsupported quantitative database schema")
117
170
  row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
118
171
  if row is None:
@@ -146,25 +199,46 @@ class QuantitativeDataset:
146
199
  connection = sqlite3.connect(path)
147
200
  connection.execute("PRAGMA foreign_keys=ON")
148
201
  connection.executescript(_SCHEMA)
149
- connection.execute("PRAGMA user_version=3")
202
+ connection.execute("PRAGMA user_version=4")
150
203
  connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
151
204
  (_json(manifest.to_dict()),))
205
+ reference_ids = {}
152
206
  for reference in manifest.references:
153
- connection.execute("INSERT INTO reference VALUES (?, ?)",
154
- (reference.key, _json(reference.to_dict())))
207
+ cursor = connection.execute("INSERT INTO reference (key, record) VALUES (?, ?)",
208
+ (reference.key, _json(reference.to_dict())))
209
+ reference_ids[reference.key] = cursor.lastrowid
210
+
211
+ @lru_cache(maxsize=131072)
212
+ def insert_target(target):
213
+ reference_key = target.reference.key
214
+ if reference_key not in reference_ids:
215
+ raise sqlite3.IntegrityError("Target reference absent from dataset")
216
+ reference_i = reference_ids[reference_key]
217
+ key = _target_key(target)
218
+ connection.execute("INSERT OR IGNORE INTO target "
219
+ "(reference_i, reference_key, key, kind, namespace, identifier) "
220
+ "VALUES (?, ?, ?, ?, ?, ?)",
221
+ (reference_i, reference_key, key, target.kind, target.namespace, target.identifier))
222
+ target_i = connection.execute("SELECT i FROM target WHERE reference_key=? AND key=?",
223
+ (reference_key, key)).fetchone()[0]
224
+ return reference_i, target_i, key
155
225
  for sample in samples:
156
226
  if not isinstance(sample, Sample):
157
227
  raise TypeError("samples must contain Samples")
158
228
  connection.execute("INSERT INTO sample VALUES (?, ?, ?)",
159
229
  (sample.reference.key, sample.sample_id, _json(sample.to_dict())))
230
+ assay_ids = {}
160
231
  for assay in assays:
161
232
  if not isinstance(assay, Assay):
162
233
  raise TypeError("assays must contain Assays")
163
234
  if assay.kind not in manifest.assay_kinds:
164
235
  raise ValueError("Assay kind absent from manifest coverage")
165
- connection.execute("INSERT INTO assay VALUES (?, ?, ?, ?)",
166
- (assay.reference.key, assay.assay_id, assay.sample_id,
167
- _json(assay.to_dict())))
236
+ reference_i = reference_ids.get(assay.reference.key)
237
+ cursor = connection.execute("INSERT INTO assay "
238
+ "(reference_i, reference_key, id, sample_id, record) VALUES (?, ?, ?, ?, ?)",
239
+ (reference_i, assay.reference.key, assay.assay_id, assay.sample_id,
240
+ _json(assay.to_dict())))
241
+ assay_ids[(reference_i, assay.assay_id, assay.sample_id)] = cursor.lastrowid
168
242
  for pair in pairings:
169
243
  if not isinstance(pair, AssayPair):
170
244
  raise TypeError("pairings must contain AssayPairs")
@@ -194,9 +268,7 @@ class QuantitativeDataset:
194
268
  if (relation.dataset_id, relation.dataset_version) != (manifest.dataset_id, manifest.version):
195
269
  raise ValueError("Target relation belongs to a different dataset release")
196
270
  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))
271
+ insert_target(target)
200
272
  for index, (first, second) in enumerate(((relation.left, relation.right), (relation.right, relation.left))):
201
273
  _check_one_to_one(relation_links[index],
202
274
  (relation.relation, first.key, second.reference.key, second.kind, second.namespace), second.key,
@@ -218,9 +290,7 @@ class QuantitativeDataset:
218
290
  if (mapping.dataset_id, mapping.dataset_version) != (manifest.dataset_id, manifest.version):
219
291
  raise ValueError("Reference mapping belongs to a different dataset release")
220
292
  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))
293
+ insert_target(target)
224
294
  connection.execute("INSERT INTO reference_mapping VALUES (?, ?, ?, ?, ?)",
225
295
  (mapping.source.reference.key, mapping.mapping_id, _target_key(mapping.source),
226
296
  mapping.target_reference.key, _json(mapping.to_dict())))
@@ -238,33 +308,39 @@ class QuantitativeDataset:
238
308
  raise ValueError("Measurement provenance names an unknown source asset")
239
309
  if source is None and not (observation.input_measurements or observation.input_references):
240
310
  raise ValueError("Measurement requires a source_asset or input measurements")
241
- reference_key = observation.target.reference.key
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)
311
+ reference_i, target_i, _ = insert_target(observation.target)
312
+ inputs = [(reference_i, identifier) for identifier in observation.input_measurements]
313
+ inputs.extend((reference_ids.get(value.reference.key), value.measurement_id)
314
+ for value in observation.input_references)
315
+ input_ids = []
244
316
  for input_reference, identifier in inputs:
245
- if connection.execute(
246
- "SELECT 1 FROM measurement WHERE reference_key=? AND id=?",
317
+ found = connection.execute(
318
+ "SELECT i FROM observation WHERE reference_i=? AND id=?",
247
319
  (input_reference, identifier),
248
- ).fetchone() is None:
320
+ ).fetchone()
321
+ if found is None:
249
322
  raise ValueError("Derived inputs must precede outputs in their declared reference contexts")
250
- target = observation.target
251
- target_key = _target_key(target)
252
- connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
253
- (reference_key, target_key, target.kind,
254
- target.namespace, target.identifier))
255
- record = observation.to_dict()
256
- record.pop("target")
257
- connection.execute("INSERT INTO measurement VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
258
- (reference_key, observation.measurement_id, target_key,
259
- observation.sample_id, observation.assay_id,
260
- observation.quantity, observation.status, _json(record)))
261
- connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?, ?)",
262
- ((reference_key, observation.measurement_id, input_reference, identifier)
263
- for input_reference, identifier in inputs))
264
- counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
323
+ input_ids.append(found[0])
324
+ assay_i = assay_ids.get((reference_i, observation.assay_id, observation.sample_id))
325
+ if assay_i is None:
326
+ raise sqlite3.IntegrityError("Observation assay/sample absent from its reference context")
327
+ cursor = connection.execute("INSERT INTO observation "
328
+ "(reference_i, id, target_i, assay_i, quantity, status, record) "
329
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
330
+ (reference_i, observation.measurement_id, target_i, assay_i,
331
+ observation.quantity, observation.status,
332
+ _json(_measurement_record(observation))))
333
+ connection.executemany("INSERT INTO observation_input VALUES (?, ?)",
334
+ ((cursor.lastrowid, input_i) for input_i in input_ids))
335
+ insert_target.cache_clear()
336
+ counts = {table: connection.execute(
337
+ f"SELECT count(*) FROM {'observation' if table == 'measurement' else table}").fetchone()[0]
265
338
  for table in ("sample", "assay", "mapping", "target", "measurement",
266
339
  "assay_pair", "target_relation", "reference_mapping")}
267
340
  connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
341
+ # Real cardinalities prevent the planner from scanning a whole
342
+ # reference in ID order for a narrow target query.
343
+ connection.execute("ANALYZE")
268
344
  connection.commit()
269
345
  connection.close()
270
346
  connection = None
@@ -344,8 +420,9 @@ class QuantitativeDataset:
344
420
  raise ValueError("Choose explicit usable mapping statuses")
345
421
  if type(require_sequence) is not bool:
346
422
  raise TypeError("require_sequence must be a boolean")
347
- if set(observation_filters) - {"sample_id", "assay_id", "quantity", "status"}:
423
+ if set(observation_filters) - {"sample_id", "assay_id", "quantity", "status", "unit", "scale", "log_base"}:
348
424
  raise ValueError("Mapped observation filters cannot override source/target reference identity")
425
+ _validate_native_selectors(**{name: observation_filters.get(name) for name in ("unit", "scale", "log_base")})
349
426
  for mapping in self.reference_mappings(target=target):
350
427
  if mapping.status not in allowed or (require_sequence and not mapping.sequence_validated):
351
428
  continue
@@ -407,16 +484,19 @@ class QuantitativeDataset:
407
484
  tuple(by_target[1].get(relation.right.key, ())))
408
485
 
409
486
  def measurements(self, *, target=None, reference=None, sample_id=None,
410
- assay_id=None, quantity=None, status=None):
487
+ assay_id=None, quantity=None, status=None, unit=None, scale=None,
488
+ log_base=None):
411
489
  """Iterate native observations; a target match is exact, never orthology.
412
490
 
413
491
  Unqualified IDs across multiple references are rejected. To browse all
414
492
  species together, omit ID filters; each result still carries its scope.
493
+ Native unit/scale/base selectors are exact; no conversion or aggregation.
415
494
  """
416
495
  if quantity is not None and quantity not in QUANTITIES:
417
496
  raise ValueError("Unknown measurement quantity")
418
497
  if status is not None and status not in MEASUREMENT_STATUSES:
419
498
  raise ValueError("Unknown measurement status")
499
+ _validate_native_selectors(unit, scale, log_base)
420
500
  scope = self._scope(reference)
421
501
  if target is not None:
422
502
  if not isinstance(target, ScopedTarget):
@@ -430,15 +510,33 @@ class QuantitativeDataset:
430
510
  filters = dict(reference_key=scope, target_key=_target_key(target) if target else None,
431
511
  sample_id=sample_id, assay_id=assay_id, quantity=quantity, status=status)
432
512
  selected = [(key, value) for key, value in filters.items() if value is not None]
433
- query = ("SELECT m.record, m.reference_key, t.kind, t.namespace, t.identifier "
513
+ query = ("SELECT m.record, m.reference_key, t.kind, t.namespace, t.identifier, "
514
+ "m.id, m.sample_id, m.assay_id, m.quantity, m.status "
434
515
  "FROM measurement m JOIN target t ON m.reference_key=t.reference_key "
435
516
  "AND m.target_key=t.key")
436
- if selected:
437
- query += " WHERE " + " AND ".join(f"m.{key}=?" for key, _ in selected)
517
+ conditions = [f"m.{key}=?" for key, _ in selected]
518
+ parameters = [value for _, value in selected]
519
+ # JSON extraction applies to original v1-v3 records and compact v4
520
+ # residual records. SQLite filters before Python reconstructs objects.
521
+ for value, expression in (
522
+ (unit, "json_extract(m.record, '$.unit')"),
523
+ (scale, "coalesce(json_extract(m.record, '$.scale'), 'linear')"),
524
+ (log_base, "json_extract(m.record, '$.log_base')"),
525
+ ):
526
+ if value is not None:
527
+ conditions.append(expression + "=?")
528
+ parameters.append(value)
529
+ if conditions:
530
+ query += " WHERE " + " AND ".join(conditions)
438
531
  query += " ORDER BY m.reference_key, m.id"
439
532
  with closing(self._connect()) as connection:
440
- for row in connection.execute(query, [value for _, value in selected]):
533
+ for row in connection.execute(query, parameters):
441
534
  values = json.loads(row[0])
535
+ if self._storage_version == 4:
536
+ values = dict(_V4_DEFAULTS, **values)
537
+ values.update(measurement_id=row[5], sample_id=row[6], assay_id=row[7],
538
+ quantity=row[8], status=row[9], dataset_id=self.manifest.dataset_id,
539
+ dataset_version=self.manifest.version)
442
540
  values["target"] = ScopedTarget(self._references[row[1]], row[2], row[4], row[3])
443
541
  if "input_references" in values:
444
542
  values["input_references"] = tuple(MeasurementReference.from_dict(value)
@@ -39,6 +39,9 @@ def run(argv=None, prog=None):
39
39
  subparser.add_argument("--assay-id")
40
40
  subparser.add_argument("--quantity")
41
41
  subparser.add_argument("--status")
42
+ subparser.add_argument("--unit", help="Exact native unit, e.g. iBAQ, rIBAQ, TPM or counts")
43
+ subparser.add_argument("--scale", choices=("linear", "log"))
44
+ subparser.add_argument("--log-base", type=float, help="Exact native logarithm base; no transformation")
42
45
  subparser.add_argument("--target")
43
46
  subparser.add_argument("--kind")
44
47
  subparser.add_argument("--namespace")
@@ -111,7 +114,8 @@ def run(argv=None, prog=None):
111
114
  references = {r.key: r for r in dataset.manifest.references}
112
115
  target = ScopedTarget(references[args.reference], args.kind, args.target, args.namespace)
113
116
  filters = dict(sample_id=args.sample, assay_id=args.assay_id,
114
- quantity=args.quantity, status=args.status)
117
+ quantity=args.quantity, status=args.status,
118
+ unit=args.unit, scale=args.scale, log_base=args.log_base)
115
119
  if args.mapped:
116
120
  if target is None:
117
121
  parser.error("--mapped requires an exact target and reference")
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orfdata
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Reference open reading frames and experimental translation evidence on annotated transcripts
5
5
  Author: OpenVax
6
6
  License-Expression: Apache-2.0
@@ -13,6 +13,7 @@ docs/pfizer-protein.md
13
13
  docs/reference.md
14
14
  docs/source-references.md
15
15
  docs/species.md
16
+ docs/storage.md
16
17
  orfdata/__init__.py
17
18
  orfdata/build.py
18
19
  orfdata/catalog.py
@@ -56,6 +57,8 @@ tests/test_pfizer.py
56
57
  tests/test_reference_context.py
57
58
  tests/test_reference_mapping.py
58
59
  tests/test_reference_orf.py
60
+ tests/test_selectors.py
61
+ tests/test_storage.py
59
62
  tests/test_uorf_catalog.py
60
63
  tests/data/copy/GSM1905474.soft
61
64
  tests/data/copy/GSM1905474_KL9_fpkm.txt.gz
@@ -92,4 +95,12 @@ tests/data/references/canis_lupus_familiaris.json
92
95
  tests/data/references/context-keys-v0.4.json
93
96
  tests/data/references/homo_sapiens.json
94
97
  tests/data/references/mus_musculus.json
95
- tests/data/references/saccharomyces_cerevisiae.json
98
+ tests/data/references/saccharomyces_cerevisiae.json
99
+ tests/data/storage/README.md
100
+ tests/data/storage/generate.py
101
+ tests/data/storage/v1.json
102
+ tests/data/storage/v1.sqlite
103
+ tests/data/storage/v2.json
104
+ tests/data/storage/v2.sqlite
105
+ tests/data/storage/v3.json
106
+ tests/data/storage/v3.sqlite
@@ -0,0 +1,15 @@
1
+ These synthetic databases were generated by the **released** ORFdata versions
2
+ 0.3.0 (storage 1), 0.4.0 (storage 2) and 0.5.0 (storage 3), using `generate.py`.
3
+ They test actual legacy layouts; changing a new database's `user_version` would
4
+ not produce a valid old-format fixture.
5
+
6
+ Each contains four species with colliding local sample/assay/target/measurement
7
+ IDs, measured zero, a censored value, uncertainty and derived input lineage.
8
+ Versions 2/3 include pairings; version 3 includes reference mappings. No records
9
+ are experimental evidence. The JSON files retain the original writer's records.
10
+
11
+ Example regeneration from a checkout of the appropriate release:
12
+
13
+ ```sh
14
+ PYTHONPATH=/path/to/orfdata-0.3.0 python generate.py /path/to/fixtures 1
15
+ ```
@@ -0,0 +1,74 @@
1
+ """Run from released 0.3.0/0.4.0/0.5.0 to generate genuine legacy databases."""
2
+
3
+ import hashlib
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import orfdata
9
+ from orfdata import (
10
+ Assay,
11
+ DatasetManifest,
12
+ Measurement,
13
+ QuantitativeDataset,
14
+ ReferenceContext,
15
+ Sample,
16
+ ScopedTarget,
17
+ SourceAsset,
18
+ )
19
+
20
+ output = Path(sys.argv[1])
21
+ version = int(sys.argv[2])
22
+ references = tuple(ReferenceContext(taxon, name, assembly, "ensembl", "110")
23
+ for taxon, name, assembly in ((9606, "human", "GRCh38"),
24
+ (10090, "mouse", "GRCm39"),
25
+ (9615, "dog", "ROS_Cfam_1.0"),
26
+ (4932, "yeast", "R64-1-1")))
27
+ source = SourceAsset("input.tsv", "https://example.invalid/storage-fixture.tsv",
28
+ hashlib.sha256(b"synthetic fixture\n").hexdigest(), 18,
29
+ "Synthetic test data; Apache-2.0", redistributable=True)
30
+ manifest = DatasetManifest("legacy-fixture", "1", "Synthetic legacy storage validation",
31
+ references, (source,), ("Synthetic, not experimental evidence",),
32
+ ("rna_seq", "digest_ms"), {"scope": "four deliberately colliding species"},
33
+ {"writer": "orfdata", "writer_version": orfdata.__version__})
34
+ samples, assays, observations, pairs, relations, mappings = [], [], [], [], [], []
35
+ for index, reference in enumerate(references):
36
+ samples.append(Sample("sample", reference, biological_unit_id="culture"))
37
+ assays.extend((Assay("rna", "sample", "rna_seq", reference), Assay("protein", "sample", "digest_ms", reference)))
38
+ gene = ScopedTarget(reference, "gene", "same-id", "fixture")
39
+ protein = ScopedTarget(reference, "protein", "same-id", "fixture")
40
+ observations.extend((
41
+ Measurement("rna-value", gene, "sample", "rna", "rna_abundance", index, "TPM",
42
+ manifest.dataset_id, manifest.version, provenance={"source_asset": "input.tsv", "row": index}),
43
+ Measurement("protein-value", protein, "sample", "protein", "protein_abundance",
44
+ None if index == 1 else 10.25 + index, "iBAQ", manifest.dataset_id, manifest.version,
45
+ status="below_detection" if index == 1 else "measured", detection_limit=20 if index == 1 else None,
46
+ uncertainty=0.5, uncertainty_type="standard_deviation",
47
+ provenance={"source_asset": "input.tsv", "row": index, "native": "<20" if index == 1 else "observed"}),
48
+ Measurement("derived", gene, "sample", "rna", "rna_abundance", 1, "fixture-units",
49
+ manifest.dataset_id, manifest.version, origin="derived", input_measurements=("rna-value",),
50
+ derivation={"method": "synthetic lineage fixture"}),
51
+ ))
52
+ if version >= 2:
53
+ from orfdata import AssayPair, TargetRelation
54
+
55
+ pairs.append(AssayPair("pair", reference, manifest.dataset_id, manifest.version,
56
+ "rna", "protein", "same_material", {"source_asset": "input.tsv"}, "concurrent"))
57
+ relations.append(TargetRelation("relation", manifest.dataset_id, manifest.version, gene, protein,
58
+ "canonical_translation", "one_to_one", {"source_asset": "input.tsv"}))
59
+ if version >= 3:
60
+ from orfdata import ReferenceMapping
61
+
62
+ mappings.append(ReferenceMapping("map", manifest.dataset_id, manifest.version,
63
+ protein, reference, (ScopedTarget(reference, "protein", "mapped-id", "fixture"),),
64
+ "unique", "synthetic identifier match", {"sequence": "not validated"},
65
+ provenance={"source_asset": "input.tsv"}))
66
+ kwargs = dict(samples=samples, assays=assays, measurements=observations)
67
+ if version >= 2:
68
+ kwargs.update(pairings=pairs, relations=relations)
69
+ if version >= 3:
70
+ kwargs.update(reference_mappings=mappings)
71
+ dataset = QuantitativeDataset.create(output / f"v{version}.sqlite", manifest, **kwargs)
72
+ expected = dict(writer_version=orfdata.__version__, manifest=manifest.to_dict(),
73
+ measurements=[m.to_dict() for m in observations], counts=dataset.counts)
74
+ (output / f"v{version}.json").write_text(json.dumps(expected, indent=2) + "\n")