Deploy initial accession explorer with a bounded GenBank annotation sample
Browse files- .gitignore +5 -0
- FEATURE_BACKLOG.md +122 -0
- README.md +64 -6
- app.py +101 -0
- catalog.py +84 -0
- data/manifest.json +30 -0
- data/sample.parquet +3 -0
- prepare_sample.py +86 -0
- requirements-data.txt +2 -0
- requirements.txt +4 -0
- tests/test_catalog.py +89 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|