1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Metric functions — numbers computed from matches.
//!
//! Every item here is a free function over flat arrays, callable without
//! constructing an evaluator:
//!
//! ```
//! use hotcoco::metrics::counts::average_precision;
//!
//! let scores = [0.9, 0.8, 0.3];
//! let matched = [true, false, true];
//! let ap = average_precision(&scores, &matched, None, 3, &[0.0, 0.5, 1.0]);
//! ```
//!
//! This is the same shape `sklearn.metrics` and `torchmetrics.functional` use,
//! and for the same reason: a metric is a pure function of its inputs, so tying
//! it to an evaluator object only makes it harder to reach.
//!
//! # `metrics` vs [`primitives`](crate::primitives)
//!
//! The split is by what a function *produces*, not by which family calls it:
//!
//! | | Produces | Contains |
//! |---|---|---|
//! | [`primitives`](crate::primitives) | matches and similarities | `sim`, `greedy`, `assign` |
//! | `metrics` | numbers from matches | `counts`, `calibration`, `confusion`, `bootstrap` |
//!
//! `primitives::greedy::greedy_match_masked` decides *which detection pairs
//! with which ground truth*. `metrics::counts::average_precision` turns that
//! decision into a number. Nothing here matches; nothing there scores.
//!
//! `tests/architecture.rs` enforces the direction of the dependency: `metrics`
//! may not import from a family driver such as [`detection`](crate::detection),
//! and `primitives` may not import from `metrics`.
//!
//! Taking `(scores, matched)` rather than a family-specific struct is what makes
//! these reusable across families: detection produces those arrays from
//! `eval_imgs`, tracking and panoptic will produce them from their own match
//! records, and the function does not know which called it.
//!
//! # Degenerate-input convention
//!
//! These free functions share one policy, the same as `primitives`:
//!
//! - **Mismatched parallel-array lengths are a programmer error** and panic via
//! `assert!` with a message naming both lengths. Nothing silently truncates,
//! no-ops, or degrades ([`primitives::assign::lsap`](crate::primitives::assign::lsap)
//! set the pattern). Each function's `# Panics` section states its checks.
//! - **Empty input is not an error** — it produces the documented empty-set
//! value (`0.0`, an empty `Vec`, an all-zero matrix), because "no detections"
//! is a legitimate evaluation state, not a bug.
//!
//! # Stability
//!
//! Provisional through 1.x, like [`primitives`](crate::primitives): public so the
//! family drivers and Python can share them, but not frozen until 1.4. Expect
//! additive change — new functions, and the tracking count vocabulary in
//! [`counts`] — rather than reshaping of what is here.
/// Whether a metric value was actually computed, as opposed to carrying the
/// crate's `-1.0` "not computed for this configuration" sentinel.
///
/// The sentinel is public contract: evaluation output uses `-1.0` for an area
/// range with no ground truth or a category absent from the split — never for a
/// genuinely low score. Filter evaluation arrays with this rather than an
/// open-coded comparison.
///
/// The predicate half of the convention whose *producer* is
/// `detection::summarize::mean_or_missing`. It lives here rather than beside the
/// producer because the lower layer reads it too — [`counts::max_f_beta`] skips
/// sentinel precisions — and `metrics` may not import a family driver.
///
/// The test is `v >= 0.0`, so `NaN` reads as **not** computed. Deliberate, and
/// not interchangeable with `!(v < 0.0)`: the two agree everywhere except `NaN`,
/// and swapping them would flip every sentinel-filtered fold on a `NaN` input.
/// The complement of [`is_computed`]: `v` is the "not computed" sentinel.