Skip to main content

tp_lib_core/models/
detection_record.rs

1//! Detection provenance record types (T008).
2//!
3//! `DetectionRecord` is appended to `PathResult.detection_provenance` for
4//! every detection ingested (applied or discarded), preserving original input
5//! order.
6
7use std::collections::BTreeMap;
8
9use chrono::{DateTime, FixedOffset};
10use serde::{Deserialize, Serialize};
11
12/// Whether a detection was punctual or linear (preserved for provenance).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum DetectionKind {
16    Punctual,
17    Linear,
18}
19
20/// Either a single timestamp (punctual) or an inclusive `[from, to]` range (linear).
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum TimestampOrRange {
24    Single {
25        timestamp: DateTime<FixedOffset>,
26    },
27    Range {
28        t_from: DateTime<FixedOffset>,
29        t_to: DateTime<FixedOffset>,
30    },
31}
32
33/// Reason a detection was discarded (FR-010, FR-011, FR-009, FR-006, FR-007, FR-007a).
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(tag = "kind", rename_all = "snake_case")]
36pub enum DiscardReason {
37    /// Timestamp / window outside the GNSS observation window.
38    OutOfTimeRange {
39        gnss_first: DateTime<FixedOffset>,
40        gnss_last: DateTime<FixedOffset>,
41    },
42    /// Coordinate-only punctual: nearest netelement is farther than the cutoff.
43    OutOfReach {
44        nearest_distance_m: f64,
45        cutoff_m: f64,
46    },
47    /// Referenced `netelement_id` does not exist in the supplied network.
48    /// (Only used for non-fatal warnings; the standard pipeline raises
49    /// `DetectionError::UnknownNetelement` instead.)
50    UnknownNetelement { netelement_id: String },
51    /// Intrinsic value out of `[0, 1]`.
52    IntrinsicOutOfRange { value: f64 },
53    /// Same timestamp + same netelement as a previously kept detection (FR-007a).
54    DuplicateOfPriorDetection { kept_index: usize },
55}
56
57/// Disposition of an ingested detection.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(tag = "status", rename_all = "snake_case")]
60pub enum DetectionStatus {
61    /// Detection was applied as a Viterbi anchor.
62    Applied {
63        netelement_id: String,
64        intrinsic: f64,
65    },
66    /// Coordinate-only detection successfully resolved within the cutoff
67    /// (subsequently applied).
68    Resolved {
69        netelement_id: String,
70        distance_m: f64,
71    },
72    /// Detection was discarded; see `reason`.
73    Discarded { reason: DiscardReason },
74}
75
76/// Per-detection provenance record (one per input detection).
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct DetectionRecord {
79    pub source_file: String,
80    pub source_row: usize,
81    pub kind: DetectionKind,
82    pub timestamp: TimestampOrRange,
83    pub status: DetectionStatus,
84    pub id: Option<String>,
85    pub source: Option<String>,
86    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
87    pub metadata: BTreeMap<String, String>,
88}