@@ -189,11 +189,17 @@ def test_feature_join_counts():
189189 assert counts ["identified_precursor_mz" ] == 2 , counts # one m/z each
190190 assert counts ["identified_proteins" ] == 2 , counts # ProtA, ProtB
191191 assert counts ["transitions_used" ] == 3 , counts
192- assert counts ["library_peptides" ] == 4 , counts # incl. orphan
192+ assert counts ["library_peptides" ] == 4 , counts # incl. orphans
193+ # Only PepA and PepB carry transitions; PepC and PepOrphan are both
194+ # declared-but-transition-less, so 4 declared splits 2 + 2.
195+ assert counts ["library_peptides_identifiable" ] == 2 , counts # PepA, PepB
196+ assert counts ["library_peptides_orphan" ] == 2 , counts # PepC, PepOrphan
193197 assert counts ["library_precursor_mz" ] == 2 , counts
194198 assert counts ["library_transitions" ] == 3 , counts
195- # 2 of 4 library peptides
196- assert counts ["library_coverage" ] == 0.5 , counts
199+ # both IDENTIFIABLE peptides were found, so coverage is 1.0 over the
200+ # identifiable base even though the library declares twice as many
201+ # peptides as can actually be searched.
202+ assert counts ["library_coverage" ] == 1.0 , counts
197203 print ("PASS: feature join (distinct peptides/precursors/proteins, coverage)" )
198204
199205
@@ -235,6 +241,10 @@ def test_empty_and_missing_inputs():
235241 assert counts ["identified_peptides" ] == 0 , counts
236242 assert counts ["library_coverage" ] == 0.0 , counts
237243 assert counts ["library_peptides" ] == 2 , counts
244+ # PepB is declared but transition-less, so only 1 peptide is
245+ # identifiable even though the library declares 2.
246+ assert counts ["library_peptides_identifiable" ] == 1 , counts
247+ assert counts ["library_peptides_orphan" ] == 1 , counts
238248
239249 for bad in (os .path .join (tmp , "nope.featureXML" ),
240250 os .path .join (tmp , "broken.TraML" )):
@@ -254,8 +264,11 @@ def test_empty_and_missing_inputs():
254264
255265def test_reference_fixture_join ():
256266 """Regression guard on the real committed OpenSwath fixtures: the vendored
257- library has 7 peptides but only 6 precursor m/z values, because one library
258- peptide carries no transitions and therefore can never be identified."""
267+ library declares 7 peptides but only 6 carry transitions, because
268+ PEPTIDEA_Extra has no transitions and therefore can never be identified.
269+ The run is perfect on every other axis (18/18 transitions, 6/6 precursor
270+ m/z), so coverage must be 6/6 = 1.0 over the identifiable subset, while
271+ the orphan stays visible as its own count instead of penalising it."""
259272 traml = os .path .join (FIXTURES , "OpenSwathWorkflow_1_input.TraML" )
260273 feat = os .path .join (FIXTURES , "OpenSwathWorkflow_1_output.featureXML" )
261274 lib = om .parse_traml (traml )
@@ -268,12 +281,91 @@ def test_reference_fixture_join():
268281 assert counts ["identified_precursor_mz" ] == 6 , counts
269282 assert counts ["identified_proteins" ] == 1 , counts
270283 assert counts ["transitions_used" ] == 18 , counts
271- assert counts ["library_coverage" ] == 0.857143 , counts
284+ # declared / identifiable / orphan must all stay observable
285+ assert counts ["library_peptides" ] == 7 , counts
286+ assert counts ["library_peptides_identifiable" ] == 6 , counts
287+ assert counts ["library_peptides_orphan" ] == 1 , counts
288+ assert counts ["library_peptides_orphan" ] == (
289+ counts ["library_peptides" ] - counts ["library_peptides_identifiable" ]), counts
290+ assert counts ["library_coverage" ] == 1.0 , counts
272291 # No PSM/FDR key may ever appear: DIA emits no spectrum-level
273292 # identifications, so such a count would be fabricated.
274293 assert not [k for k in counts if "psm" in k .lower ()], counts
275294 assert not [k for k in counts if "fdr" in k .lower ()], counts
276- print ("PASS: reference fixture join (7 peptides / 6 precursor m/z / 6 identified)" )
295+ print ("PASS: reference fixture join (7 declared / 6 identifiable / 6 identified "
296+ "-> coverage 1.0)" )
297+
298+
299+ def test_orphan_peptide_does_not_penalise_coverage ():
300+ """F5 regression guard: a declared-but-transition-less peptide must not
301+ lower library_coverage.
302+
303+ The fixture library carries one such orphan (PEPTIDEA_Extra). Two runs
304+ that find every IDENTIFIABLE peptide must both report coverage 1.0,
305+ whether or not the library also declares orphans. Before this guard the
306+ metric divided by all declared peptides, so the same perfect run reported
307+ 6/7 = 0.857143 and looked like a 14% identification failure caused by the
308+ tool. If this test ever fails, either the denominator changed again or the
309+ orphan counts changed -- both require the module docstring to change too.
310+ """
311+ traml = os .path .join (FIXTURES , "OpenSwathWorkflow_1_input.TraML" )
312+ feat = os .path .join (FIXTURES , "OpenSwathWorkflow_1_output.featureXML" )
313+ lib = om .parse_traml (traml )
314+ counts = om .count_identifications (lib , feat )
315+
316+ # every identifiable peptide was found
317+ assert counts ["identified_peptides" ] == counts ["library_peptides_identifiable" ], \
318+ counts
319+ # so coverage is exactly 1.0 despite the orphan being declared
320+ assert counts ["library_peptides_orphan" ] > 0 , counts # the orphan is present
321+ assert counts ["library_coverage" ] == 1.0 , counts
322+
323+ # Same result with an orphan-free library: 3 declared, 3 identifiable,
324+ # all 3 identified -> coverage 1.0. Identifiable coverage is therefore
325+ # independent of how many orphans the library happens to declare.
326+ with tempfile .TemporaryDirectory () as tmp :
327+ lib2 = os .path .join (tmp , "noorphan.TraML" )
328+ make_traml (
329+ lib2 ,
330+ proteins = [("ProtA" , "uniprot_a" )],
331+ peptides = [("PepA" , "AAAA" , "2" , "ProtA" ),
332+ ("PepB" , "BBBB" , "2" , "ProtA" ),
333+ ("PepC" , "CCCC" , "2" , "ProtA" )],
334+ transitions = [("t1" , "PepA" , "400.5" ),
335+ ("t2" , "PepB" , "500.5" ),
336+ ("t3" , "PepC" , "600.5" )],
337+ )
338+ feat2 = os .path .join (tmp , "all.featureXML" )
339+ make_featurexml (feat2 , [["t1" ], ["t2" ], ["t3" ]])
340+ c2 = om .count_identifications (om .parse_traml (lib2 ), feat2 )
341+ assert c2 ["library_peptides" ] == 3 , c2
342+ assert c2 ["library_peptides_identifiable" ] == 3 , c2
343+ assert c2 ["library_peptides_orphan" ] == 0 , c2
344+ assert c2 ["library_coverage" ] == 1.0 , c2
345+
346+ # A genuine miss must still be penalised: drop one peptide's feature and
347+ # coverage falls below 1.0. This is what stops the orphan exclusion from
348+ # degenerating into "coverage is always 1.0".
349+ with tempfile .TemporaryDirectory () as tmp :
350+ lib3 = os .path .join (tmp , "miss.TraML" )
351+ make_traml (
352+ lib3 ,
353+ proteins = [("ProtA" , "uniprot_a" )],
354+ peptides = [("PepA" , "AAAA" , "2" , "ProtA" ),
355+ ("PepB" , "BBBB" , "2" , "ProtA" ),
356+ ("PepC" , "CCCC" , "2" , "ProtA" )],
357+ transitions = [("t1" , "PepA" , "400.5" ),
358+ ("t2" , "PepB" , "500.5" ),
359+ ("t3" , "PepC" , "600.5" )],
360+ )
361+ feat3 = os .path .join (tmp , "partial.featureXML" )
362+ make_featurexml (feat3 , [["t1" ], ["t2" ]]) # PepC not identified
363+ c3 = om .count_identifications (om .parse_traml (lib3 ), feat3 )
364+ assert c3 ["identified_peptides" ] == 2 , c3
365+ assert c3 ["library_peptides_identifiable" ] == 3 , c3
366+ assert c3 ["library_coverage" ] == 0.666667 , c3
367+
368+ print ("PASS: orphan peptide does not penalise coverage; real misses still do" )
277369
278370
279371def test_isobaric_peptides_share_one_precursor_mz ():
@@ -325,6 +417,7 @@ def main():
325417 test_unmatched_transition_fails_loudly ()
326418 test_empty_and_missing_inputs ()
327419 test_reference_fixture_join ()
420+ test_orphan_peptide_does_not_penalise_coverage ()
328421 test_isobaric_peptides_share_one_precursor_mz ()
329422 print ("\n All openswath_metrics tests passed." )
330423
0 commit comments