cgeorgiaw HF Staff commited on
Commit
97d5f73
·
verified ·
1 Parent(s): 0f2093d

Deploy initial accession explorer with a bounded GenBank annotation sample

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ .venv/
3
+ .env
4
+ .cache/
5
+ *.log
FEATURE_BACKLOG.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Feature backlog
2
+
3
+ Track desired features that are not yet implemented. Add new ideas here and update each item's status as work progresses. These entries describe future work; implementation details and priorities remain open unless explicitly agreed.
4
+
5
+ Last updated: 2026-09-29.
6
+
7
+ ## Overview
8
+
9
+ | ID | Feature | Status |
10
+ | --- | --- | --- |
11
+ | F001 | GenBank inference coverage map | Planned; not implemented |
12
+ | F002 | Taxonomy map and navigation | Planned; not implemented |
13
+ | F003 | Comparison with RefSeq annotations | Planned; not implemented |
14
+ | F004 | Download annotations by accession, individually or in bulk | Planned; only individual sampled segment downloads exist |
15
+ | F005 | Fast accession lookup and retrieval across the full dataset | Planned; only a local sample index exists |
16
+
17
+ ## F001 — GenBank inference coverage map
18
+
19
+ **Goal:** Show how much of GenBank we currently have annotations for, and how that coverage grows as more inference completes.
20
+
21
+ Desired behavior:
22
+
23
+ - Provide a visual map or overview of coverage, with a way to explore covered and uncovered groups.
24
+ - Show counts and proportions for annotated assemblies and bases, with the underlying totals visible.
25
+ - Distinguish partial coverage from complete coverage, and distinguish the full annotation collection from the small sample loaded by the prototype.
26
+ - Refresh the map as new inference outputs are published. Display when coverage was last updated.
27
+ - Let users navigate from the overview to available assemblies and their annotations.
28
+
29
+ Decisions and dependencies for implementation:
30
+
31
+ - Define the GenBank inventory or snapshot used as the denominator, including any eligibility filters. Label these explicitly so the map's meaning stays clear as GenBank changes.
32
+ - Define what counts as complete inference, including how to handle missing contigs, segments, or bases.
33
+ - Build a coverage inventory from published outputs and inference metadata, deduplicating assembly versions and repeated runs.
34
+ - Choose the visualization and refresh mechanism. Historical progress views are an optional extension.
35
+
36
+ ## F002 — Taxonomy map and navigation
37
+
38
+ **Goal:** Provide a taxonomy-based view of the available genomes and annotations.
39
+
40
+ Desired behavior:
41
+
42
+ - Explore organisms through an interactive taxonomy map, with navigation from broad groups to individual taxa and assemblies.
43
+ - Show how many assemblies or annotations are available within a selected group.
44
+ - Let users open accession-level results from the map.
45
+ - Consider sharing coverage information with F001 so users can see which taxonomic groups have been processed.
46
+
47
+ Decisions and dependencies for implementation:
48
+
49
+ - Choose a visualization, such as an expandable tree, treemap, or sunburst; no format is selected yet.
50
+ - Choose and version a taxonomy source, and resolve assembly taxon IDs to lineages.
51
+ - Define how to represent missing, unresolved, or changed taxon IDs.
52
+
53
+ ## F003 — Comparison with RefSeq annotations
54
+
55
+ **Goal:** For sequences with RefSeq annotations, show where our predictions agree with or differ from those annotations.
56
+
57
+ Desired behavior:
58
+
59
+ - Display RefSeq annotation tracks alongside our strand-specific CDS probabilities in the same genomic region.
60
+ - Highlight differences, with candidate categories including predicted coding regions outside RefSeq CDS features, RefSeq CDS regions with low predicted probability, and boundary or strand disagreements.
61
+ - Let users inspect the underlying RefSeq features and prediction values for each difference.
62
+ - Show the RefSeq annotation version and inference provenance used in the comparison.
63
+ - Make unavailable or incompatible comparisons explicit.
64
+
65
+ Decisions and dependencies for implementation:
66
+
67
+ - Resolve GenBank records to the corresponding RefSeq assemblies and contigs, checking sequence versions and coordinate compatibility before overlaying tracks.
68
+ - Decide how to handle sequences that require coordinate mapping rather than a direct overlay.
69
+ - Define how probability tracks become comparable predicted intervals, including thresholds, strand handling, and boundary tolerance.
70
+ - Select comparison metrics and feature scope, including how multiple transcripts and overlapping CDS features are treated.
71
+ - Describe differences as disagreements with the reference annotations; a disagreement alone does not establish which annotation is correct.
72
+
73
+ ## F004 — Download annotations by accession
74
+
75
+ **Goal:** Make it easy for users to download annotations for the assemblies or contigs they are interested in.
76
+
77
+ Current baseline: the prototype can download one selected sampled segment as Parquet. Accession-level and bulk downloads are future work.
78
+
79
+ Desired behavior:
80
+
81
+ - Offer a download action directly from accession search results and assembly or contig views.
82
+ - Let users select multiple accessions or paste/upload an accession list, then download the matching annotations together.
83
+ - Include all available segments for a selected contig, and all available contigs for a selected assembly. Clearly flag incomplete coverage.
84
+ - Show which requested accessions matched, were unavailable, or need a version choice. Record the exact versions included.
85
+ - Include metadata, coordinate conventions, inference provenance, and a manifest of the exported contents.
86
+ - Show estimated download size and progress; support preparing large exports without blocking browsing.
87
+
88
+ Decisions and dependencies for implementation:
89
+
90
+ - Choose export formats and packaging. Preserve original per-base probabilities in Parquet; consider additional formats for derived intervals once their interpretation is defined.
91
+ - Define handling of duplicates, multiple inference runs, accession versions, and partial failures in bulk requests.
92
+ - Decide whether sequence is included when available, and label its availability explicitly.
93
+ - Define export size limits, temporary storage, and download expiration.
94
+ - Use F005 to locate and retrieve requested records efficiently across the full collection.
95
+
96
+ ## F005 — Fast lookup and retrieval across the full dataset
97
+
98
+ **Goal:** Keep accession searches and annotation retrieval responsive as the app expands beyond its bundled sample.
99
+
100
+ Current baseline: the prototype indexes local sample metadata and reads selected Parquet row groups. It does not search the full bucket.
101
+
102
+ Desired behavior:
103
+
104
+ - Resolve assembly and contig accessions through a persistent metadata index, without scanning every annotation shard for each query.
105
+ - Map each match to its exact accession version, inference run, bucket object, row group, and segment coordinates.
106
+ - Retrieve only the data needed for the selected records or region, where the storage layout supports it.
107
+ - Support bulk resolution for F004, and paginate large assembly results.
108
+ - Update the index as inference outputs are published, showing the indexed snapshot or refresh time.
109
+ - Distinguish an accession absent from the indexed inventory from an incomplete or stale index and from a retrieval failure.
110
+
111
+ Decisions and dependencies for implementation:
112
+
113
+ - Benchmark lookup latency, data transfer, and memory use on representative workloads before selecting an index backend or changing the storage layout.
114
+ - Evaluate an accession-to-object/row-group index and metadata queries separately from fetching the large probability arrays.
115
+ - Check remote range-read support and actual row-group sizes. A coordinate index alone does not guarantee efficient region reads; consider smaller genomic chunks or another layout if needed.
116
+ - Define incremental indexing and consistent publication so index entries refer to available, identifiable object versions. Handle replacements and repeated inference runs explicitly.
117
+ - Consider bounded caching of frequently accessed metadata and annotation chunks, with invalidation tied to data versions.
118
+ - Share versioned inventory metadata with F001 and F002 where useful, keeping coverage summaries and lookup results consistent.
119
+
120
+ ## Adding and updating entries
121
+
122
+ Give each new feature a stable ID, a goal, a status, desired behavior, and open decisions or dependencies. Link implementation work when it starts. Once shipped, mark the entry implemented and record any remaining scope rather than deleting its history.
README.md CHANGED
@@ -1,13 +1,71 @@
1
  ---
2
- title: Genbank Annotation Explorer
3
- emoji: 👀
4
- colorFrom: blue
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.29.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: GenBank Annotation Explorer
3
+ emoji: 🧬
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.6.0
8
+ python_version: '3.10'
9
  app_file: app.py
10
  pinned: false
11
+ short_description: Explore sampled genome annotations by accession ID
12
  ---
13
 
14
+ Search a small, real subset of [HuggingFaceBio/genbank-annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations) by assembly accession, contig accession, or the exact `source_key`. View strand-specific CDS probabilities, inspect provenance, and download original segment rows as Parquet.
15
+
16
+ Future features and open design decisions are tracked in the [feature backlog](FEATURE_BACKLOG.md).
17
+
18
+ ## Run locally
19
+
20
+ ```bash
21
+ python -m venv .venv
22
+ source .venv/bin/activate
23
+ pip install -r requirements.txt
24
+ python app.py
25
+ ```
26
+
27
+ Open http://localhost:7860. The bundled sample works offline and requires no token at runtime.
28
+
29
+ ## Deploy to a Space
30
+
31
+ The preview Space is [HuggingFaceBio/genbank-annotation-explorer](https://huggingface.co/spaces/HuggingFaceBio/genbank-annotation-explorer), with private visibility.
32
+
33
+ To deploy or update it, upload this directory, including `data/sample.parquet` and `data/manifest.json`. The README metadata configures the runtime, following the [Gradio Spaces documentation](https://huggingface.co/docs/hub/spaces-sdks-gradio). Keep the Space private for the initial preview: the bundled annotations will be accessible to its viewers.
34
+
35
+ ## Lookup behavior
36
+
37
+ - Exact versioned accessions match only that version; unknown versions do not fall back.
38
+ - Unversioned accessions return all matching versions in the sample. Input ignores case and surrounding whitespace.
39
+ - Assembly lookups return all sampled contigs/segments for that assembly. This sample is not a complete assembly.
40
+ - A miss means “not in this sample,” not “absent from GenBank/the bucket.”
41
+ - Split contigs stay as separate segments, preserving their source coordinates and segment counts.
42
+
43
+ ## Data and coordinates
44
+
45
+ The bucket contains model predictions: positive- and negative-strand per-base P(CDS), metadata, and sometimes sequence. It does not provide curated gene names or gene models. Coordinates are 0-based, end-exclusive. The initial viewport shows up to 10,000 bases; update the start/end fields to explore other regions. Large windows are averaged into at most 1,200 bins per strand. Downloads retain original probabilities and source columns without downsampling.
46
+
47
+ `data/manifest.json` records the bucket object, content hash, source row indices, selection limits, and sample checksum. The default sample is limited to 24 complete segments and 2 million bases from one fungi chunk. It is for interface development, not a representative biological sample. Full per-base columns are loaded only for the selected segment; the accession index holds metadata only.
48
+
49
+ ## Rebuild the sample
50
+
51
+ Use a Hugging Face login or `HF_TOKEN` with bucket read access. Never commit tokens.
52
+
53
+ ```bash
54
+ pip install -r requirements-data.txt
55
+ hf auth login
56
+ python prepare_sample.py
57
+ ```
58
+
59
+ The default source object is about 31 MB; automatic downloads are capped at 100 MB. Override `--source`, `--max-rows`, or `--max-bp` to choose another bounded sample. If the source already exists locally:
60
+
61
+ ```bash
62
+ python prepare_sample.py --local-file /path/to/source.parquet
63
+ ```
64
+
65
+ Source rows are selected intact, without truncating their probabilities. The app never scans or downloads the full bucket at startup. To support arbitrary accessions across the full bucket later, build a separate metadata index mapping accession → bucket object → row group, then retrieve matching probability data on demand.
66
+
67
+ ## Checks
68
+
69
+ ```bash
70
+ python -m unittest discover -s tests -v
71
+ ```
app.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A small Hugging Face Space for exploring genome annotations by accession."""
2
+ from pathlib import Path
3
+ import os
4
+ import tempfile
5
+
6
+ # Shared compute hosts can have /tmp/gradio owned by a different user.
7
+ os.environ.setdefault("GRADIO_TEMP_DIR", str(Path(tempfile.gettempdir()) / f"genbank-explorer-{os.getuid()}"))
8
+
9
+ import gradio as gr
10
+ import pyarrow.parquet as pq
11
+
12
+ from catalog import Catalog
13
+
14
+
15
+ def build_app(catalog=None):
16
+ catalog = catalog or Catalog()
17
+ all_ids = list(range(len(catalog.records)))
18
+ first = catalog.records[0]
19
+
20
+ def search(accession):
21
+ ids = catalog.lookup(accession)
22
+ choices = [(f"{catalog.records[i]['record_name']} · {catalog.records[i]['assembly_accession']} · "
23
+ f"[{catalog.records[i]['segment_start_bp']:,}, {catalog.records[i]['segment_end_bp']:,})", str(i)) for i in ids]
24
+ if not str(accession or "").strip():
25
+ message = "Enter an assembly or contig accession. Try an example below."
26
+ elif not ids:
27
+ message = "No match in this sample. This does not mean the accession is absent from the full bucket."
28
+ else:
29
+ message = f"Found **{len(ids)} sampled segment(s)**. Assembly results include only contigs loaded in this prototype."
30
+ return message, catalog.table(ids), gr.Dropdown(choices=choices, value=str(ids[0]) if ids else None), None
31
+
32
+ def select_segment(index):
33
+ if index is None:
34
+ return {}, None, None, None, "Choose a matching segment.", None
35
+ record = catalog.records[int(index)]
36
+ start = record["segment_start_bp"]
37
+ end = min(record["segment_end_bp"], start + 10_000)
38
+ plot, step = catalog.window(index, start, end)
39
+ return record, start, end, plot, plot_note(step), None
40
+
41
+ def plot_note(step):
42
+ return ("Coordinates are **0-based, end-exclusive**. "
43
+ + ("Each point is one base." if step == 1 else f"Each point is the mean of up to **{step:,} bases**; short peaks can be smoothed.")
44
+ + " Download the segment for the original per-base probabilities.")
45
+
46
+ def update_window(index, start, end):
47
+ if index is None:
48
+ raise gr.Error("Look up an accession and choose a segment first.")
49
+ try:
50
+ plot, step = catalog.window(index, start, end)
51
+ except (ValueError, TypeError, OverflowError) as exc:
52
+ raise gr.Error(str(exc)) from exc
53
+ return plot, plot_note(step)
54
+
55
+ def export(index):
56
+ if index is None:
57
+ raise gr.Error("Choose a segment first.")
58
+ target = Path(tempfile.mkdtemp(prefix="genbank-export-")) / "annotation.parquet"
59
+ pq.write_table(catalog.segment_table(index), target, compression="zstd")
60
+ return str(target)
61
+
62
+ with gr.Blocks(title="GenBank Annotation Explorer", delete_cache=(3600, 3600)) as demo:
63
+ gr.Markdown("# GenBank Annotation Explorer\nExplore predicted coding regions by **assembly or contig accession**.")
64
+ gr.Markdown(f"**Prototype sample · {len(catalog.records):,} segments · "
65
+ f"{len(catalog.manifest['assemblies']):,} assemblies · {catalog.manifest['bases']:,} bases**\n\n"
66
+ "Source: [HuggingFaceBio/genbank-annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations). "
67
+ "These are model-predicted CDS probabilities, not curated gene features. Search covers the loaded sample only.")
68
+ with gr.Row():
69
+ accession = gr.Textbox(label="Accession ID", placeholder="Assembly (GCA_…) or contig accession", scale=5)
70
+ search_button = gr.Button("Find annotations", variant="primary", scale=1)
71
+ gr.Examples(examples=[[first["assembly_accession"]], [first["record_name"]],
72
+ [first["record_name"].rsplit(".", 1)[0]]], inputs=accession)
73
+ status = gr.Markdown("Enter an accession or select an example. IDs are case-insensitive; version suffixes are optional.")
74
+ results = gr.Dataframe(value=catalog.table([]), interactive=False, label="Matching segments")
75
+ segment = gr.Dropdown(choices=[], label="Segment to explore", interactive=True)
76
+ with gr.Row():
77
+ start = gr.Number(label="Start (0-based, inclusive)", precision=0)
78
+ end = gr.Number(label="End (exclusive)", precision=0)
79
+ view = gr.Button("Update region")
80
+ plot = gr.LinePlot(x="Position (bp)", y="P(CDS)", color="Strand", y_lim=[0, 1],
81
+ title="CDS probability by strand", height=320)
82
+ note = gr.Markdown("Choose a segment to see its probability tracks.")
83
+ with gr.Accordion("Segment metadata and provenance", open=False):
84
+ metadata = gr.JSON(label="Source metadata")
85
+ with gr.Row():
86
+ download = gr.Button("Prepare segment download")
87
+ file = gr.File(label="Original segment annotations (Parquet)", interactive=False)
88
+ with gr.Accordion("Accessions available in this sample", open=False):
89
+ gr.Dataframe(value=catalog.table(all_ids), interactive=False)
90
+ gr.JSON(value=catalog.manifest, label="Sample provenance")
91
+ outputs = [status, results, segment, file]
92
+ for event in (search_button.click, accession.submit):
93
+ event(search, accession, outputs).then(select_segment, segment, [metadata, start, end, plot, note, file])
94
+ segment.input(select_segment, segment, [metadata, start, end, plot, note, file])
95
+ view.click(update_window, [segment, start, end], [plot, note])
96
+ download.click(export, segment, file)
97
+ return demo
98
+
99
+
100
+ if __name__ == "__main__":
101
+ build_app().queue(default_concurrency_limit=2).launch(server_name="0.0.0.0")
catalog.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Accession lookup and lazy, segment-level access to the local sample."""
2
+ from collections import defaultdict
3
+ import json
4
+ from pathlib import Path
5
+ import re
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import pyarrow.parquet as pq
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ PROBS = ["pred_prob_positive_strand_cds", "pred_prob_negative_strand_cds"]
13
+ DISPLAY_COLUMNS = ["assembly_accession", "record_name", "organism_name", "division",
14
+ "segment_start_bp", "segment_end_bp", "segment_index", "segment_count"]
15
+
16
+
17
+ def normalize(value):
18
+ return str(value or "").strip().upper()
19
+
20
+
21
+ def unversioned(value):
22
+ return re.sub(r"\.\d+$", "", value)
23
+
24
+
25
+ class Catalog:
26
+ def __init__(self, directory=ROOT / "data"):
27
+ directory = Path(directory)
28
+ self.path = directory / "sample.parquet"
29
+ self.manifest = json.loads((directory / "manifest.json").read_text())
30
+ pf = pq.ParquetFile(self.path)
31
+ columns = [c for c in pf.schema_arrow.names if c not in PROBS and c != "sequence"]
32
+ self.records = pf.read(columns=columns).to_pylist()
33
+ self.locations = [(g, r) for g in range(pf.num_row_groups)
34
+ for r in range(pf.metadata.row_group(g).num_rows)]
35
+ self.exact, self.base = defaultdict(set), defaultdict(set)
36
+ for i, record in enumerate(self.records):
37
+ for field in ("assembly_accession", "record_name", "source_key"):
38
+ key = normalize(record[field])
39
+ if key:
40
+ self.exact[key].add(i)
41
+ if field != "source_key":
42
+ self.base[unversioned(key)].add(i)
43
+
44
+ def lookup(self, accession):
45
+ key = normalize(accession)
46
+ if not key:
47
+ return []
48
+ # A versioned query never silently falls back to another version.
49
+ matches = self.exact.get(key, set()) if re.search(r"\.\d+$", key) or "|" in key else self.base.get(key, set())
50
+ return sorted(matches, key=lambda i: (self.records[i]["assembly_accession"],
51
+ self.records[i]["record_name"],
52
+ self.records[i]["segment_index"]))
53
+
54
+ def table(self, ids):
55
+ return pd.DataFrame([self.records[i] for i in ids], columns=DISPLAY_COLUMNS)
56
+
57
+ def segment_table(self, index):
58
+ index = int(index)
59
+ if not 0 <= index < len(self.records):
60
+ raise ValueError("Choose a loaded segment.")
61
+ group, row = self.locations[index]
62
+ return pq.ParquetFile(self.path).read_row_group(group).slice(row, 1)
63
+
64
+ def window(self, index, start=None, end=None, max_points=1200):
65
+ record = self.records[int(index)]
66
+ lo, hi = record["segment_start_bp"], record["segment_end_bp"]
67
+ start = lo if start is None else int(start)
68
+ end = min(hi, start + 10_000) if end is None else int(end)
69
+ if not lo <= start < end <= hi:
70
+ raise ValueError(f"Enter a range within [{lo:,}, {hi:,}) with start < end.")
71
+ table = self.segment_table(index)
72
+ width = end - start
73
+ step = max(1, (width + max_points - 1) // max_points)
74
+ positions = np.arange(start, end, step)
75
+ frames = []
76
+ for column, strand in zip(PROBS, ["+ strand", "− strand"]):
77
+ values = table.column(column)[0].values.to_numpy(zero_copy_only=False).astype(np.float32)
78
+ if len(values) != hi - lo:
79
+ raise ValueError("Probability length does not match segment coordinates.")
80
+ values = values[start - lo:end - lo]
81
+ counts = np.minimum(step, width - np.arange(0, width, step))
82
+ means = np.add.reduceat(values, np.arange(0, width, step)) / counts
83
+ frames.append(pd.DataFrame({"Position (bp)": positions, "P(CDS)": means, "Strand": strand}))
84
+ return pd.concat(frames, ignore_index=True), step
data/manifest.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bucket_id": "HuggingFaceBio/genbank-annotations",
3
+ "source_path": "annotations/fungi/s0000/s0000.chunk-00510.parquet",
4
+ "source_xet_hash": "f7454034faf6e7e0c9f80f51355269eafbf5f8447b3aa41166fb7558797dafb9",
5
+ "created_at": "2026-09-29T17:05:37.431500+00:00",
6
+ "selection": "First complete segments fitting the row and base budgets, in source order",
7
+ "max_rows": 24,
8
+ "max_bp": 2000000,
9
+ "rows": 12,
10
+ "bases": 1969534,
11
+ "source_rows": 48,
12
+ "source_row_indices": [
13
+ 0,
14
+ 1,
15
+ 2,
16
+ 3,
17
+ 4,
18
+ 5,
19
+ 6,
20
+ 7,
21
+ 8,
22
+ 9,
23
+ 11,
24
+ 46
25
+ ],
26
+ "assemblies": [
27
+ "GCA_055472895.1"
28
+ ],
29
+ "sha256": "f1128a4ee3215ceb51fe9ddbd6ae2228ce20e56a60579ee9b22011b945460b52"
30
+ }
data/sample.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f1128a4ee3215ceb51fe9ddbd6ae2228ce20e56a60579ee9b22011b945460b52
3
+ size 6999680
prepare_sample.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extract a bounded, reproducible sample from one published bucket object."""
2
+ import argparse
3
+ from datetime import datetime, timezone
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+ import tempfile
8
+
9
+ import pyarrow as pa
10
+ import pyarrow.parquet as pq
11
+
12
+ BUCKET = "HuggingFaceBio/genbank-annotations"
13
+ DEFAULT_SOURCE = "annotations/fungi/s0000/s0000.chunk-00510.parquet"
14
+ ROOT = Path(__file__).resolve().parent
15
+
16
+
17
+ def extract(source, output, source_path, max_rows=24, max_bp=2_000_000, xet_hash=None):
18
+ if max_rows < 1 or max_bp < 1:
19
+ raise ValueError("Sample limits must be positive.")
20
+ pf = pq.ParquetFile(source)
21
+ selected, bases = [], 0
22
+ # Read lengths before loading the much larger per-base columns.
23
+ lengths = pf.read(columns=["segment_bp_length"]).column(0).to_pylist()
24
+ for i, length in enumerate(lengths):
25
+ if 0 < length <= max_bp - bases:
26
+ selected.append(i)
27
+ bases += length
28
+ if len(selected) >= max_rows:
29
+ break
30
+ if not selected:
31
+ raise ValueError("No complete segments fit the sample budget.")
32
+ pieces, offset = [], 0
33
+ for group in range(pf.num_row_groups):
34
+ count = pf.metadata.row_group(group).num_rows
35
+ indices = [i - offset for i in selected if offset <= i < offset + count]
36
+ if indices:
37
+ pieces.append(pf.read_row_group(group).take(pa.array(indices)))
38
+ offset += count
39
+ sample = pa.concat_tables(pieces)
40
+ output = Path(output)
41
+ output.mkdir(parents=True, exist_ok=True)
42
+ target = output / "sample.parquet"
43
+ # One group per segment lets the app fetch just the selected segment.
44
+ pq.write_table(sample, target, compression="zstd", row_group_size=1)
45
+ metadata = sample.select([c for c in sample.column_names if not c.startswith("pred_prob_") and c != "sequence"]).to_pylist()
46
+ manifest = {
47
+ "bucket_id": BUCKET, "source_path": source_path, "source_xet_hash": xet_hash,
48
+ "created_at": datetime.now(timezone.utc).isoformat(),
49
+ "selection": "First complete segments fitting the row and base budgets, in source order",
50
+ "max_rows": max_rows, "max_bp": max_bp, "rows": sample.num_rows,
51
+ "bases": bases, "source_rows": pf.metadata.num_rows, "source_row_indices": selected,
52
+ "assemblies": sorted({r["assembly_accession"] for r in metadata}),
53
+ "sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
54
+ }
55
+ (output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
56
+ print(json.dumps(manifest, indent=2))
57
+ return manifest
58
+
59
+
60
+ def main():
61
+ parser = argparse.ArgumentParser(description=__doc__)
62
+ parser.add_argument("--source", default=DEFAULT_SOURCE)
63
+ parser.add_argument("--local-file", type=Path)
64
+ parser.add_argument("--output", type=Path, default=ROOT / "data")
65
+ parser.add_argument("--max-rows", type=int, default=24)
66
+ parser.add_argument("--max-bp", type=int, default=2_000_000)
67
+ args = parser.parse_args()
68
+ if args.local_file:
69
+ extract(args.local_file, args.output, args.source, args.max_rows, args.max_bp)
70
+ else:
71
+ from huggingface_hub import HfApi
72
+ api = HfApi()
73
+ entries = api.list_bucket_tree(BUCKET, args.source, recursive=False)
74
+ entry = next((e for e in entries if e.path == args.source), None)
75
+ if entry is None:
76
+ raise ValueError(f"Bucket object not found: {args.source}")
77
+ if entry.size > 100_000_000:
78
+ raise ValueError("Source exceeds the 100 MB download budget. Choose a smaller object or use --local-file.")
79
+ with tempfile.TemporaryDirectory() as temp:
80
+ local = Path(temp) / "source.parquet"
81
+ api.download_bucket_files(BUCKET, [(entry, local)], raise_on_missing_files=True)
82
+ extract(local, args.output, args.source, args.max_rows, args.max_bp, entry.xet_hash)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
requirements-data.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ -r requirements.txt
2
+ huggingface_hub==1.16.0
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==6.6.0
2
+ numpy>=1.26,<3
3
+ pandas>=2.2,<3
4
+ pyarrow>=18,<24
tests/test_catalog.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+ from pathlib import Path
4
+ import tempfile
5
+ import unittest
6
+
7
+ import numpy as np
8
+ import pyarrow as pa
9
+ import pyarrow.parquet as pq
10
+
11
+ from catalog import Catalog, ROOT
12
+
13
+
14
+ class SampleTests(unittest.TestCase):
15
+ @classmethod
16
+ def setUpClass(cls):
17
+ cls.catalog = Catalog()
18
+
19
+ def test_sample_integrity_and_budget(self):
20
+ manifest = self.catalog.manifest
21
+ self.assertEqual(hashlib.sha256(self.catalog.path.read_bytes()).hexdigest(), manifest['sha256'])
22
+ self.assertEqual(len(self.catalog.records), manifest['rows'])
23
+ self.assertLessEqual(manifest['rows'], manifest['max_rows'])
24
+ self.assertLessEqual(manifest['bases'], manifest['max_bp'])
25
+ for i, record in enumerate(self.catalog.records):
26
+ row = self.catalog.segment_table(i).to_pylist()[0]
27
+ length = record['segment_end_bp'] - record['segment_start_bp']
28
+ for key in ('pred_prob_positive_strand_cds', 'pred_prob_negative_strand_cds'):
29
+ values = np.asarray(row[key], dtype=float)
30
+ self.assertEqual(len(values), length)
31
+ self.assertTrue(np.all(np.isfinite(values)))
32
+ self.assertTrue(np.all((values >= 0) & (values <= 1)))
33
+
34
+ def test_real_accession_lookup(self):
35
+ first = self.catalog.records[0]
36
+ self.assertIn(0, self.catalog.lookup(first['source_key']))
37
+ self.assertIn(0, self.catalog.lookup(' ' + first['record_name'].lower() + ' '))
38
+ self.assertIn(0, self.catalog.lookup(first['record_name'].rsplit('.', 1)[0]))
39
+ expected = [i for i, r in enumerate(self.catalog.records) if r['assembly_accession'] == first['assembly_accession']]
40
+ self.assertCountEqual(self.catalog.lookup(first['assembly_accession']), expected)
41
+ self.assertEqual(self.catalog.lookup(first['record_name'].rsplit('.', 1)[0] + '.999999'), [])
42
+ self.assertEqual(self.catalog.lookup(''), [])
43
+ self.assertEqual(self.catalog.lookup('not-an-accession'), [])
44
+
45
+
46
+ class CoordinateTests(unittest.TestCase):
47
+ def setUp(self):
48
+ self.temp = tempfile.TemporaryDirectory()
49
+ directory = Path(self.temp.name)
50
+ rows = []
51
+ for version in (1, 2):
52
+ rows.append({'assembly_accession': f'GCA_000001.{version}',
53
+ 'record_name': f'TEST000001.{version}',
54
+ 'source_key': f'GCA_000001.{version}|TEST000001.{version}',
55
+ 'segment_index': 1, 'segment_start_bp': 100,
56
+ 'segment_end_bp': 105,
57
+ 'pred_prob_positive_strand_cds': [0., 0.2, 0.4, 0.6, 0.8],
58
+ 'pred_prob_negative_strand_cds': [1., 0.8, 0.6, 0.4, 0.2]})
59
+ pq.write_table(pa.Table.from_pylist(rows), directory / 'sample.parquet')
60
+ (directory / 'manifest.json').write_text('{}')
61
+ self.catalog = Catalog(directory)
62
+
63
+ def tearDown(self):
64
+ self.temp.cleanup()
65
+
66
+ def test_version_resolution(self):
67
+ self.assertEqual(self.catalog.lookup('GCA_000001'), [0, 1])
68
+ self.assertEqual(self.catalog.lookup('GCA_000001.2'), [1])
69
+ self.assertEqual(self.catalog.lookup('TEST000001'), [0, 1])
70
+ self.assertEqual(self.catalog.lookup('GCA_000001.3'), [])
71
+
72
+ def test_absolute_coordinates_and_partial_last_bin(self):
73
+ frame, step = self.catalog.window(0, 100, 105, max_points=2)
74
+ self.assertEqual(step, 3)
75
+ positive = frame[frame.Strand == '+ strand']
76
+ self.assertEqual(positive['Position (bp)'].tolist(), [100, 103])
77
+ np.testing.assert_allclose(positive['P(CDS)'], [0.2, 0.7], atol=1e-6)
78
+ frame, step = self.catalog.window(0, 103, 105)
79
+ self.assertEqual(step, 1)
80
+ np.testing.assert_allclose(frame[frame.Strand == '+ strand']['P(CDS)'], [0.6, 0.8])
81
+
82
+ def test_invalid_ranges(self):
83
+ for start, end in [(99, 103), (100, 106), (103, 103), (104, 102)]:
84
+ with self.assertRaises(ValueError):
85
+ self.catalog.window(0, start, end)
86
+
87
+
88
+ if __name__ == '__main__':
89
+ unittest.main()