@@ -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 )
0 commit comments