99
1010from krakenparser .utils import ensure_output_dir
1111
12- # Maps Kraken2 single-letter rank codes to MPA prefixes
13- _RANK_PREFIX = {
14- "D" : "d" ,
15- "K" : "k" ,
16- "P" : "p" ,
17- "C" : "c" ,
18- "O" : "o" ,
19- "F" : "f" ,
20- "G" : "g" ,
21- "S" : "s" ,
22- }
23-
2412_log = logging .getLogger (__name__ )
2513
14+ _MAIN_LVLS = {"R" , "K" , "D" , "P" , "C" , "O" , "F" , "G" , "S" }
15+
2616
27- def _parse_line (line : str ) :
17+ def _parse_line (line : str , remove_spaces : bool = False ) -> list :
2818 """
2919 Parse one Kraken2 report line.
3020
31- Standard format (6 columns):
32- pct cum_reads direct_reads rank taxid name(indented)
33-
34- Returns (name, depth, rank, cum_reads, pct) or None on malformed input.
21+ Returns [name, level_num, level_type, all_reads, percents]
22+ or empty list on malformed input.
3523 """
3624 parts = line .rstrip ("\n " ).split ("\t " )
37- if len (parts ) < 5 :
38- return None
25+ if len (parts ) < 4 :
26+ return []
3927 try :
40- pct = float (parts [0 ])
41- cum_reads = int (parts [1 ])
28+ int (parts [1 ])
4229 except ValueError :
43- return None
30+ return []
4431
45- rank = parts [3 ].strip ()
46- name_field = parts [- 1 ] # always the last column regardless of format variant
32+ try :
33+ percents = float (parts [0 ])
34+ except ValueError :
35+ return []
36+ all_reads = int (parts [1 ])
37+
38+ try :
39+ int (parts [- 3 ])
40+ level_type = parts [- 2 ].strip ()
41+ map_kuniq = {
42+ "species" : "S" ,
43+ "genus" : "G" ,
44+ "family" : "F" ,
45+ "order" : "O" ,
46+ "class" : "C" ,
47+ "phylum" : "P" ,
48+ "superkingdom" : "D" ,
49+ "kingdom" : "K" ,
50+ }
51+ if level_type not in map_kuniq :
52+ level_type = "-"
53+ else :
54+ level_type = map_kuniq [level_type ]
55+ except ValueError :
56+ level_type = parts [- 3 ].strip ()
4757
48- depth = 0
49- for ch in name_field :
58+ name = parts [- 1 ]
59+ spaces = 0
60+ for ch in name :
5061 if ch == " " :
51- depth += 1
62+ spaces += 1
5263 else :
5364 break
54- name = name_field .strip ()
65+ name = name .strip ()
66+ if remove_spaces :
67+ name = name .replace (" " , "_" )
5568
56- return name , depth // 2 , rank , cum_reads , pct
69+ level_num = spaces / 2
70+ return [name , level_num , level_type , all_reads , percents ]
5771
5872
5973def kreport_to_mpa (
@@ -67,54 +81,63 @@ def kreport_to_mpa(
6781 """
6882 Convert a single Kraken2 report to MPA format.
6983
70- Uses a stack to track the current taxonomic path. Each entry is
71- (structural_depth, mpa_segment, is_standard_rank). When a node at
72- depth d is encountered, all stack entries with depth >= d are popped
73- before the new entry is pushed, keeping the path consistent.
84+ Tracks the current taxonomic path via curr_path and prev_lvl_num,
85+ popping the stack when moving back up the tree — exactly as the
86+ original script does.
7487 """
7588 if not Path (report_path ).is_file ():
7689 raise FileNotFoundError (f"Input file not found: { report_path } " )
7790 out_path = ensure_output_dir (output_path , is_file = True )
78- # Stack entries: (structural_depth, mpa_segment, is_standard_rank)
79- stack : list [tuple [int , str , bool ]] = []
91+
92+ curr_path : list [str ] = []
93+ prev_lvl_num = - 1
8094
8195 with open (report_path ) as r_fh , open (out_path , "w" ) as o_fh :
8296 if display_header :
8397 o_fh .write ("#Classification\t " + os .path .basename (report_path ) + "\n " )
8498
8599 for line in r_fh :
86- parsed = _parse_line (line )
87- if parsed is None :
100+ report_vals = _parse_line (line , remove_spaces )
101+ if len ( report_vals ) < 5 :
88102 continue
89- name , depth , rank , cum_reads , pct = parsed
90103
91- # Skip unclassified and root — never appear in MPA output
92- if rank in ("U" , "R" ):
104+ name , level_num , level_type , all_reads , percents = report_vals
105+
106+ # Пропускаем unclassified
107+ if level_type == "U" :
93108 continue
94109
95- # Strip numeric suffix to get base rank (e.g. "S1" → "S", "G2" → "G")
96- rank_base = rank .rstrip ("0123456789" )
97- is_standard = rank_base in _RANK_PREFIX and rank == rank_base
110+ # Нормализуем тип уровня
111+ if level_type not in _MAIN_LVLS :
112+ level_type = "x"
113+ elif level_type == "K" :
114+ level_type = "k"
115+ elif level_type == "D" :
116+ level_type = "d"
98117
99- if not is_standard and not include_intermediate :
100- continue
118+ level_str = level_type .lower () + "__" + name
101119
102- prefix = _RANK_PREFIX .get (rank_base , "x" )
103- seg_name = name .replace (" " , "_" ) if remove_spaces else name
104- mpa_seg = f"{ prefix } __{ seg_name } "
120+ if prev_lvl_num == - 1 :
121+ prev_lvl_num = level_num
122+ curr_path .append (level_str )
123+ continue
105124
106- # Trim stack to the current structural depth
107- while stack and stack [- 1 ][0 ] >= depth :
108- stack .pop ()
109- stack .append ((depth , mpa_seg , is_standard ))
125+ while level_num != (prev_lvl_num + 1 ):
126+ prev_lvl_num -= 1
127+ curr_path .pop ()
110128
111- # Build the full MPA path; omit intermediate (x__) segments when not requested
112- path = "|" .join (
113- seg for (_ , seg , std ) in stack if include_intermediate or std
114- )
129+ if (level_type == "x" and include_intermediate ) or level_type != "x" :
130+ ancestors = [
131+ seg
132+ for seg in curr_path
133+ if (seg [0 ] != "x" or include_intermediate ) and seg [0 ] != "r"
134+ ]
135+ path = "|" .join (ancestors + [level_str ])
136+ value = str (all_reads ) if use_reads else str (percents )
137+ o_fh .write (path + "\t " + value + "\n " )
115138
116- value = str ( cum_reads ) if use_reads else str ( pct )
117- o_fh . write ( f" { path } \t { value } \n " )
139+ curr_path . append ( level_str )
140+ prev_lvl_num = level_num
118141
119142
120143def main () -> None :
@@ -137,7 +160,6 @@ def main() -> None:
137160 dest = "input_dir" ,
138161 help = "Input directory containing Kraken2 report files (batch mode)" ,
139162 )
140-
141163 parser .add_argument (
142164 "-o" ,
143165 "--output" ,
@@ -212,7 +234,7 @@ def main() -> None:
212234 continue
213235 out_name = f .name .replace (".kreport" , ".MPA.TXT" )
214236 kreport_to_mpa (str (f ), str (output_dir / out_name ), ** kwargs )
215- _log .info (f "Converted to MPA successfully. Output stored in { output_dir } " )
237+ _log .info ("Converted to MPA successfully. Output stored in %s" , output_dir )
216238 else :
217239 kreport_to_mpa (args .r_file , args .o_file , ** kwargs )
218240
0 commit comments