Skip to content

Commit 7029fc3

Browse files
authored
Merge pull request #26 from plantcad/feat/ncbi-utr-derivation
BUSCO evaluation
2 parents fab4bbc + 486d25a commit 7029fc3

8 files changed

Lines changed: 152 additions & 18 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1+
---
2+
title: GeneCAD
3+
emoji: 🧬
4+
colorFrom: green
5+
colorTo: blue
6+
sdk: gradio
7+
sdk_version: "5.33.0"
8+
app_file: app.py
9+
pinned: false
10+
license: apache-2.0
11+
python_version: "3.12"
12+
---
13+
114
<div align="center">
215

316
# GeneCAD: Plant Genome Annotation with a DNA Foundation Model
417

5-
![](https://img.shields.io/badge/version-0.2.0-blue)
18+
![](https://img.shields.io/badge/version-0.2.2-blue)
619
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
720
[![CI](https://github.com/plantcad/genecad/actions/workflows/ci.yaml/badge.svg)](https://github.com/plantcad/genecad/actions/workflows/ci.yaml)
821
[![bioRxiv](https://img.shields.io/badge/bioRxiv-10.1101/2025.10.31.685877-b31b1b.svg)](https://doi.org/10.1101/2025.10.31.685877)

predict.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ MIN_TRANSCRIPT_LENGTH="3"
8181
CPU_WORKERS="1"
8282
LAUNCHER_ARG="${LAUNCHER:-}"
8383
MODEL_CHECKPOINT_ARG=""
84+
BASE_MODEL_ARG=""
8485

8586
while [[ $# -gt 0 ]]; do
8687
case $1 in
@@ -95,6 +96,7 @@ while [[ $# -gt 0 ]]; do
9596
-g|--gpus) GPUS_ARG="$2"; shift 2 ;;
9697
--launcher) LAUNCHER_ARG="$2"; shift 2 ;;
9798
--model-checkpoint) MODEL_CHECKPOINT_ARG="$2"; shift 2 ;;
99+
--base-model) BASE_MODEL_ARG="$2"; shift 2 ;;
98100
-h|--help) usage ;;
99101
*) echo "Unknown option: $1"; usage ;;
100102
esac
@@ -152,6 +154,10 @@ esac
152154
if [[ -n "$MODEL_CHECKPOINT_ARG" ]]; then
153155
HEAD_MODEL="$MODEL_CHECKPOINT_ARG"
154156
fi
157+
if [[ -n "$BASE_MODEL_ARG" ]]; then
158+
BASE_MODEL="$BASE_MODEL_ARG"
159+
TOKENIZER_PATH="$BASE_MODEL"
160+
fi
155161

156162
# =================================================================
157163
# GPU Resolution

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "genecad"
7-
version = "0.2.1"
7+
version = "0.2.2"
88
description = "GeneCAD: A long-context genome annotation model built on PlantCAD2"
99
readme = "README.md"
1010
requires-python = ">=3.12,<3.13"

scripts/evaluate.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,12 @@ def eval_splice_sites(pred_exon_genes, genome):
495495
def extract_transcripts(pred_exon_genes, genome, output_path):
496496
"""
497497
Extract spliced transcript sequences directly from the predicted exon
498-
structure — replaces the need for gffread.
498+
structure — replaces the need for gffread. Transcripts containing
499+
ambiguous bases are excluded because older MetaEuk/MMseqs versions can
500+
crash while translating them.
499501
"""
500-
count = skipped = 0
502+
count = skipped = skipped_ambiguous = 0
503+
valid_bases = set("ACGTacgt")
501504
with open(output_path, "w") as fh:
502505
for gdata in pred_exon_genes.values():
503506
ch, st = gdata["chrom"], gdata["strand"]
@@ -512,11 +515,20 @@ def extract_transcripts(pred_exon_genes, genome, output_path):
512515
tx_seq = "".join(seq[s - 1 : e] for (s, e) in exons)
513516
if st == "-":
514517
tx_seq = _revcomp(tx_seq)
518+
if not set(tx_seq) <= valid_bases:
519+
skipped_ambiguous += 1
520+
continue
515521
fh.write(f">{tx_id}\n{tx_seq}\n")
516522
count += 1
517523
print(f"[INFO] Extracted {count} transcript sequences.", file=sys.stderr)
518524
if skipped:
519525
print(f"[WARN] Skipped {skipped} genes (chrom not in FASTA).", file=sys.stderr)
526+
if skipped_ambiguous:
527+
print(
528+
f"[WARN] Skipped {skipped_ambiguous} transcripts containing "
529+
"ambiguous bases for BUSCO.",
530+
file=sys.stderr,
531+
)
520532
return count
521533

522534

scripts/extract.py

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,26 +216,87 @@ def _extract_gff_features(
216216
# For now, we use the longest transcript as the canonical transcript
217217
transcript_is_canonical = is_longest_transcript(transcript)
218218

219-
# Process each feature
219+
# Collect sub-features by type for this transcript
220+
exon_sub_features: list[SeqFeature] = []
221+
cds_sub_features: list[SeqFeature] = []
222+
utr_sub_features: list[SeqFeature] = []
220223
for feature in transcript.sub_features:
221-
# Skip exon features if requested (they are structural, not modeling features)
222-
if skip_exon_features and feature.type == "exon":
223-
continue
224-
225-
if feature.type not in [
224+
if feature.type == "exon":
225+
exon_sub_features.append(feature)
226+
elif feature.type == GffFeatureType.CDS:
227+
cds_sub_features.append(feature)
228+
elif feature.type in (
226229
GffFeatureType.FIVE_PRIME_UTR,
227-
GffFeatureType.CDS,
228230
GffFeatureType.THREE_PRIME_UTR,
229-
]:
231+
):
232+
utr_sub_features.append(feature)
233+
else:
230234
raise ValueError(
231235
f"Found unexpected transcript feature type: {feature.type}"
232236
)
233237

238+
# Derive UTRs from exon-CDS when explicit UTR features are absent
239+
derived_utr_features: list[tuple[int, int, GffFeatureType]] = []
240+
if (
241+
not skip_exon_features
242+
and exon_sub_features
243+
and cds_sub_features
244+
and not utr_sub_features
245+
):
246+
cds_coords = [
247+
(int(f.location.start), int(f.location.end))
248+
for f in cds_sub_features
249+
]
250+
cds_min = min(s for s, _ in cds_coords)
251+
cds_max = max(e for _, e in cds_coords)
252+
strand = transcript_info.strand
253+
for ef in exon_sub_features:
254+
es = int(ef.location.start)
255+
ee = int(ef.location.end)
256+
if strand == 1:
257+
if es < cds_min:
258+
derived_utr_features.append(
259+
(
260+
es,
261+
min(ee, cds_min),
262+
GffFeatureType.FIVE_PRIME_UTR,
263+
)
264+
)
265+
if ee > cds_max:
266+
derived_utr_features.append(
267+
(
268+
max(es, cds_max),
269+
ee,
270+
GffFeatureType.THREE_PRIME_UTR,
271+
)
272+
)
273+
else:
274+
if ee > cds_max:
275+
derived_utr_features.append(
276+
(
277+
max(es, cds_max),
278+
ee,
279+
GffFeatureType.FIVE_PRIME_UTR,
280+
)
281+
)
282+
if es < cds_min:
283+
derived_utr_features.append(
284+
(
285+
es,
286+
min(ee, cds_min),
287+
GffFeatureType.THREE_PRIME_UTR,
288+
)
289+
)
290+
291+
# Process CDS and UTR features (explicit or derived)
292+
modeling_features: list[SeqFeature] = (
293+
cds_sub_features + utr_sub_features
294+
)
295+
for feature in modeling_features:
234296
feature_id = get_feature_id(feature)
235297
feature_info = get_position_info(feature)
236298
feature_name = get_feature_name(feature)
237299

238-
# Create a validated GeneFeatureData object
239300
feature_data = SequenceFeature(
240301
species_id=species_id,
241302
species_name=species_name,
@@ -263,6 +324,38 @@ def _extract_gff_features(
263324
)
264325
features_data.append(feature_data)
265326

327+
for feat_start, feat_stop, feat_type in derived_utr_features:
328+
if feat_start >= feat_stop:
329+
continue
330+
strand_char = "+" if transcript_info.strand == 1 else "-"
331+
derived_id = f"{feat_type}:{transcript_id}:{feat_start}:{feat_stop}:{strand_char}"
332+
feature_data = SequenceFeature(
333+
species_id=species_id,
334+
species_name=species_name,
335+
chromosome_id=chrom_id,
336+
chromosome_name=chrom_name,
337+
chromosome_length=chrom_length,
338+
gene_id=gene_id,
339+
gene_name=gene_name,
340+
gene_strand=gene_info.strand,
341+
gene_start=gene_info.start,
342+
gene_stop=gene_info.stop,
343+
transcript_id=transcript_id,
344+
transcript_name=transcript_name,
345+
transcript_strand=transcript_info.strand,
346+
transcript_is_canonical=transcript_is_canonical,
347+
transcript_start=transcript_info.start,
348+
transcript_stop=transcript_info.stop,
349+
feature_id=derived_id,
350+
feature_name=None,
351+
feature_strand=transcript_info.strand,
352+
feature_type=feat_type,
353+
feature_start=feat_start,
354+
feature_stop=feat_stop,
355+
filename=filename,
356+
)
357+
features_data.append(feature_data)
358+
266359
logger.info(
267360
f"[species={species_config.id}] Total features extracted: {len(features_data)}"
268361
)

scripts/install_release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ set -euo pipefail
88
# GENECAD_VERSION=0.2.0
99
# PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu128
1010

11-
GENECAD_VERSION="${GENECAD_VERSION:-0.2.0}"
11+
GENECAD_VERSION="${GENECAD_VERSION:-0.2.2}"
1212
PYTORCH_CUDA_INDEX="${PYTORCH_CUDA_INDEX:-https://download.pytorch.org/whl/cu128}"
1313
WHEEL_URL="https://github.com/plantcad/genecad/releases/download/v${GENECAD_VERSION}/genecad-${GENECAD_VERSION}-py3-none-any.whl"
1414

src/reelprotein.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,9 @@ def build_cds_string_for_gene(gene, chrom_seq):
110110
raw_seq = chrom_seq[a - 1 : b]
111111
seq_str = str(raw_seq).upper()
112112
if strand == "+":
113-
trimmed = seq_str[cds["phase"] :]
113+
parts.append(seq_str)
114114
else:
115-
rc = str(Seq(seq_str).reverse_complement())
116-
trimmed = rc[cds["phase"] :]
117-
parts.append(trimmed)
115+
parts.append(str(Seq(seq_str).reverse_complement()))
118116

119117
return "".join(parts)
120118

train.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ DATA_DIR="$PIPELINE_DIR/data/$DOMAIN"
325325
GFF_DIR="$DATA_DIR/gff/training"
326326
FASTA_DIR="$DATA_DIR/fasta/training"
327327

328+
# Default: skip exon features (Ensembl/plant GFFs have explicit UTR annotations).
329+
# Set to "no" for NCBI-style GFFs that lack UTR features; extract.py will then
330+
# derive five_prime_UTR / three_prime_UTR from exon-CDS difference.
331+
GFF_SKIP_EXON="yes"
332+
328333
if [[ -z "$ANIMAL_INPUT_DIR" ]]; then
329334
ANIMAL_INPUT_DIR="$FASTA_DIR"
330335
fi
@@ -686,6 +691,11 @@ else
686691
fi
687692
done
688693

694+
# Custom NCBI GFFs lack explicit UTR features; derive them from exon-CDS in extract.py.
695+
if [[ $contains_ensembl -eq 0 ]]; then
696+
GFF_SKIP_EXON="no"
697+
fi
698+
689699
if [[ $contains_ensembl -eq 1 ]]; then
690700
mkdir -p "$ANIMAL_GFF_DIR" "$ANIMAL_INPUT_DIR"
691701

@@ -867,6 +877,7 @@ if [ ! -f "$EXTRACT_DIR/raw_features.parquet" ]; then
867877
--input-dir "$GFF_DIR" \
868878
--species-id "$sid" \
869879
--output "$out_file" \
880+
--skip-exon-features "$GFF_SKIP_EXON" \
870881
"${SPECIES_CONFIG_ARG[@]}"
871882
) &
872883

@@ -903,6 +914,7 @@ PY
903914
--input-dir "$GFF_DIR" \
904915
--species-id $SPECIES_IDS \
905916
--output "$EXTRACT_DIR/raw_features.parquet" \
917+
--skip-exon-features "$GFF_SKIP_EXON" \
906918
"${SPECIES_CONFIG_ARG[@]}"
907919
fi
908920
echo " Created: raw_features.parquet"

0 commit comments

Comments
 (0)