@@ -55,8 +55,11 @@ def main():
5555 sys .exit (1 )
5656
5757 # Data structures
58- gene_strands = defaultdict (lambda : {"+" : set (), "-" : set ()})
59- parent_strands = defaultdict (lambda : {"+" : set (), "-" : set ()})
58+ # UPDATED: track unknown strands '.' and '?' too
59+ gene_strands = defaultdict (lambda : {"+" : set (), "-" : set (), "." : set (), "?" : set ()})
60+ parent_strands = defaultdict (
61+ lambda : {"+" : set (), "-" : set (), "." : set (), "?" : set ()}
62+ )
6063 gene_to_transcripts = defaultdict (list ) # gene_id -> [transcript line nums]
6164 transcript_children = defaultdict (
6265 list
@@ -140,15 +143,17 @@ def main():
140143 if fid and pid and ftype not in ["exon" , "CDS" ]:
141144 transcript_to_gene [fid ] = pid
142145 gene_to_transcripts [pid ].append (line_num )
143- if strand in ["+" , "-" ]:
146+ # UPDATED: track '.', '?' too
147+ if strand in ["+" , "-" , "." , "?" ]:
144148 gene_strands [pid ][strand ].add (line_num )
145149
146150 # Classify exon/CDS children per transcript
147151 if pid and ftype in ["exon" , "CDS" ]:
148152 transcript_children [pid ].append (line_num )
149153
150154 # Track strand of any child under its parent
151- if pid and strand in ["+" , "-" ]:
155+ # UPDATED: track '.', '?' too
156+ if pid and strand in ["+" , "-" , "." , "?" ]:
152157 parent_strands [pid ][strand ].add (line_num )
153158
154159 # Helpers
@@ -174,6 +179,17 @@ def is_exempt_parent(pid):
174179 # - the parent ID is in exempt_parent_ids (any of its occurrences had exception=trans-splicing)
175180 return args .allow_trans_splicing and pid in exempt_parent_ids
176181
182+ # NEW: strand conflict rule:
183+ # conflict if:
184+ # - both '+' and '-' appear; OR
185+ # - any unknown ('.' or '?') appears together with any known ('+' or '-')
186+ def has_strand_conflict (strands_dict ):
187+ has_plus = bool (strands_dict .get ("+" ))
188+ has_minus = bool (strands_dict .get ("-" ))
189+ has_known = has_plus or has_minus
190+ has_unk = bool (strands_dict .get ("." )) or bool (strands_dict .get ("?" ))
191+ return (has_plus and has_minus ) or (has_known and has_unk )
192+
177193 # 0) Duplicate ID detection & cascading removals (only if enabled, and only for PARENT features)
178194 if args .check_dup_ids :
179195 # Only consider IDs that have children (i.e., appear as a Parent= somewhere)
@@ -246,17 +262,26 @@ def is_exempt_parent(pid):
246262 parts = feature_lines [line_num - 1 ][1 ].split ("\t " )
247263 if len (parts ) < 3 or parts [2 ] not in ("gene" , "ncRNA_gene" ):
248264 continue # only check for real gene entries
249- if strands ["+" ] and strands ["-" ]:
250- print (f"Error: Gene '{ gene_id } ' has transcripts on both + and - strands." )
265+
266+ # UPDATED: use has_strand_conflict (includes unknown '.'/'?')
267+ if has_strand_conflict (strands ):
268+ print (
269+ f"Error: Gene '{ gene_id } ' has transcripts with conflicting strands (mix of +/-, or unknown with known)."
270+ )
251271 for ln in sorted (strands ["+" ]):
252272 print (f" + (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
253273 for ln in sorted (strands ["-" ]):
254274 print (f" - (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
275+ for ln in sorted (strands ["." ]):
276+ print (f" . (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
277+ for ln in sorted (strands ["?" ]):
278+ print (f" ? (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
255279 print ()
256280 if args .remove :
257- lines_to_remove .update (strands ["+" ] | strands ["-" ])
281+ # UPDATED: remove all strand buckets involved
282+ lines_to_remove .update (strands ["+" ] | strands ["-" ] | strands ["." ] | strands ["?" ])
258283 affected_genes .add (gene_id )
259- for ln in strands ["+" ] | strands ["-" ]:
284+ for ln in ( strands ["+" ] | strands ["-" ] | strands [ "." ] | strands [ "?" ]) :
260285 attrs = feature_lines [ln - 1 ][1 ].split ("\t " )[8 ]
261286 tm = re .search (r"ID=([^;]+)" , attrs )
262287 if tm :
@@ -284,7 +309,8 @@ def is_exempt_parent(pid):
284309 if parts [2 ] not in ("gene" , "ncRNA_gene" ):
285310 continue
286311 strand = parts [6 ]
287- if strand in ["+" , "-" ]:
312+ # UPDATED: include '.' and '?'
313+ if strand in ["+" , "-" , "." , "?" ]:
288314 gene_occ_strands .append (strand )
289315
290316 if not gene_occ_strands :
@@ -299,7 +325,8 @@ def is_exempt_parent(pid):
299325 ctype , cstrand = cparts [2 ], cparts [6 ]
300326 if ctype not in ("exon" , "CDS" ):
301327 continue
302- if cstrand not in ["+" , "-" ]:
328+ # UPDATED: include '.' and '?'
329+ if cstrand not in ["+" , "-" , "." , "?" ]:
303330 continue
304331
305332 # If ANY gene occurrence has a different strand than this child, it’s a conflict
@@ -309,13 +336,13 @@ def is_exempt_parent(pid):
309336 if bad_children :
310337 parent_strand_summary = "," .join (sorted (set (gene_occ_strands )))
311338 print (
312- f"Error: Gene '{ gene_id } ' (strand(s) { parent_strand_summary } ) has direct child features on the opposite strand ."
339+ f"Error: Gene '{ gene_id } ' (strand(s) { parent_strand_summary } ) has direct child features on conflicting strands ."
313340 )
314341 for ln in sorted (bad_children ):
315342 print (f" child (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
316343 print ()
317344 if args .remove :
318- # Remove those opposite-strand children and any of their descendants
345+ # Remove those conflicting children and any of their descendants
319346 stack = list (bad_children )
320347 while stack :
321348 cur = stack .pop ()
@@ -345,15 +372,24 @@ def is_exempt_parent(pid):
345372 if parent_type in ("gene" , "ncRNA_gene" ):
346373 continue # already handled above
347374
348- if strands ["+" ] and strands ["-" ]:
349- print (f"Error: Parent '{ parent_id } ' has child features on both strands." )
375+ # UPDATED: use has_strand_conflict to include '.'/'?' mixing with +/- as conflict
376+ if has_strand_conflict (strands ):
377+ print (
378+ f"Error: Parent '{ parent_id } ' has child features with conflicting strands (mix of +/-, or unknown with known)."
379+ )
350380 for ln in sorted (strands ["+" ]):
351381 print (f" + (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
352382 for ln in sorted (strands ["-" ]):
353383 print (f" - (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
384+ for ln in sorted (strands ["." ]):
385+ print (f" . (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
386+ for ln in sorted (strands ["?" ]):
387+ print (f" ? (line { ln } ): { feature_lines [ln - 1 ][1 ]} " )
354388 print ()
355389 if args .remove :
356- lines_to_remove .update (strands ["+" ] | strands ["-" ])
390+ lines_to_remove .update (
391+ strands ["+" ] | strands ["-" ] | strands ["." ] | strands ["?" ]
392+ )
357393 # Also drop the parent itself to avoid orphan children mix
358394 if parent_ln :
359395 lines_to_remove .add (parent_ln )
@@ -490,3 +526,4 @@ def is_exempt_parent(pid):
490526
491527if __name__ == "__main__" :
492528 main ()
529+
0 commit comments