Skip to main content

hydra_engine_wds/analysis/
artifact.rs

1use crate::io::analysis_io::AnalysisArtifact;
2
3/// Error returned when serialising or deserialising an [`AnalysisArtifact`].
4#[derive(Debug)]
5pub enum AnalysisBytesError {
6    /// The JSON payload is malformed or does not match the expected schema.
7    Json(serde_json::Error),
8}
9
10impl std::fmt::Display for AnalysisBytesError {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        match self {
13            Self::Json(e) => write!(f, "analysis artifact JSON error: {e}"),
14        }
15    }
16}
17
18impl std::error::Error for AnalysisBytesError {}
19
20impl From<serde_json::Error> for AnalysisBytesError {
21    fn from(value: serde_json::Error) -> Self {
22        Self::Json(value)
23    }
24}
25
26/// Encode an analysis artifact to canonical JSON bytes.
27///
28/// The produced bytes should be written to `analysis.json` alongside the
29/// `results.out` file for the same run. The file is versioned via
30/// `schema_version`; callers must treat it as read-only and must not
31/// recompute heavy analytics at render time.
32///
33/// If `results.out` (or the INP that produced it) changes, the corresponding
34/// `analysis.json` must be deleted as stale before a new artifact is produced.
35pub fn encode_analysis_artifact(
36    artifact: &AnalysisArtifact,
37) -> Result<Vec<u8>, AnalysisBytesError> {
38    Ok(serde_json::to_vec_pretty(artifact)?)
39}
40
41/// Decode canonical JSON bytes into an analysis artifact.
42pub fn decode_analysis_artifact(bytes: &[u8]) -> Result<AnalysisArtifact, AnalysisBytesError> {
43    Ok(serde_json::from_slice(bytes)?)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::io::analysis_io::{
50        AnalysisArtifact, AnalysisSource, ContinuousDistribution, DistributionSet, HistogramBin,
51        StatusDistribution, SummaryStats, ANALYSIS_SCHEMA_VERSION,
52    };
53
54    #[test]
55    fn round_trip_analysis_bytes() {
56        let artifact = AnalysisArtifact {
57            schema_version: ANALYSIS_SCHEMA_VERSION,
58            source: AnalysisSource {
59                output_file: "network.out".to_string(),
60                report_file: "report.json".to_string(),
61                period_count: 25,
62            },
63            distributions: DistributionSet {
64                pressure: ContinuousDistribution {
65                    bins: vec![HistogramBin {
66                        start: 0.0,
67                        end: 10.0,
68                        count: 3,
69                    }],
70                    summary: SummaryStats {
71                        min: 0.0,
72                        max: 10.0,
73                        mean: 4.0,
74                        p05: 0.5,
75                        p25: 2.0,
76                        p50: 4.0,
77                        p75: 6.0,
78                        p95: 9.5,
79                    },
80                    thresholds: None,
81                },
82                head: ContinuousDistribution::default(),
83                flow: ContinuousDistribution::default(),
84                velocity: ContinuousDistribution::default(),
85                status: StatusDistribution {
86                    open: 10,
87                    closed: 2,
88                    active: 1,
89                    other: 0,
90                },
91            },
92            demand_reliability: None,
93            service_compliance: None,
94        };
95
96        let bytes = encode_analysis_artifact(&artifact).expect("encode artifact");
97        let decoded = decode_analysis_artifact(&bytes).expect("decode artifact");
98
99        assert_eq!(decoded.schema_version, ANALYSIS_SCHEMA_VERSION);
100        assert_eq!(decoded.source.period_count, 25);
101        assert_eq!(decoded.distributions.pressure.bins.len(), 1);
102        assert_eq!(decoded.distributions.status.open, 10);
103    }
104}