Skip to main content

hydra_engine_wds/simulation/
types.rs

1use crate::hydraulics::HydraulicError;
2use crate::quality::QualityError;
3use crate::ValidationError;
4
5// SimWarning, WarningKind, NodeQuantity, and LinkQuantity are defined in
6// crate::io so that the output writers in that crate can reference them
7// without a circular dependency.
8pub use crate::io::{LinkQuantity, NodeQuantity, SimWarning, WarningKind};
9
10// ── Error types ───────────────────────────────────────────────────────────────
11
12/// Errors returned by the session API (§8.4).
13#[derive(Debug, Clone)]
14pub enum SessionError {
15    // ── Fatal pre-simulation ─────────────────────────────────────────────────
16    /// The data model failed one or more validation checks (§2.9).
17    ValidationFailed(Vec<ValidationError>),
18    /// The requested object ID does not exist in the loaded network.
19    UnknownId(String),
20    /// A result was requested for a time that has no recorded snapshot.
21    NoSnapshotAtTime {
22        /// The requested simulation time (seconds).
23        requested_t: f64,
24    },
25    /// The operation is not valid in the current session phase.
26    InvalidPhase {
27        /// Phase name expected for this operation.
28        expected: String,
29        /// Actual phase at the time of the call.
30        actual: String,
31    },
32    // ── Fatal mid-simulation ─────────────────────────────────────────────────
33    /// The hydraulic solver encountered an error.
34    HydraulicSolve(HydraulicError),
35    /// The quality engine encountered an error.
36    QualityEngine(QualityError),
37}
38
39impl std::fmt::Display for SessionError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::ValidationFailed(errs) => {
43                write!(f, "validation failed: {} error(s)", errs.len())
44            }
45            Self::UnknownId(id) => write!(f, "unknown object ID: '{id}'"),
46            Self::NoSnapshotAtTime { requested_t } => {
47                write!(f, "no result snapshot at t={requested_t}")
48            }
49            Self::InvalidPhase { expected, actual } => {
50                write!(f, "invalid phase: expected {expected}, actual {actual}")
51            }
52            Self::HydraulicSolve(e) => write!(f, "hydraulic solver error: {e}"),
53            Self::QualityEngine(e) => write!(f, "quality engine error: {e:?}"),
54        }
55    }
56}
57
58impl std::error::Error for SessionError {}
59
60// ── Settable property enums ───────────────────────────────────────────────────
61
62/// Settable node properties via `set_node_property`.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum NodeProperty {
65    /// Override elevation (internal length unit).
66    Elevation,
67    /// Initial water quality.
68    InitialQuality,
69}
70
71/// Settable link properties via `set_link_property`.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum LinkProperty {
74    /// Override pipe roughness.
75    Roughness,
76    /// Override initial status (0 = Closed, 1 = Open).
77    InitialStatus,
78    /// Override initial setting (pump speed or valve setpoint).
79    InitialSetting,
80}
81
82// ── Session phases ────────────────────────────────────────────────────────────
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub(super) enum Phase {
86    /// Session allocated; no network loaded.
87    Created,
88    /// Network loaded and validated; hydraulic simulation not yet started.
89    Loaded,
90    /// Hydraulic EPS complete.
91    HydraulicsDone,
92    /// Hydraulic + quality EPS both complete.
93    QualityDone,
94}
95
96impl Phase {
97    pub(super) fn name(self) -> &'static str {
98        match self {
99            Phase::Created => "Created",
100            Phase::Loaded => "Loaded",
101            Phase::HydraulicsDone => "HydraulicsDone",
102            Phase::QualityDone => "QualityDone",
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::hydraulics::HydraulicError;
111    use crate::quality::QualityError;
112
113    #[test]
114    fn phase_name_all_variants() {
115        assert_eq!(Phase::Created.name(), "Created");
116        assert_eq!(Phase::Loaded.name(), "Loaded");
117        assert_eq!(Phase::HydraulicsDone.name(), "HydraulicsDone");
118        assert_eq!(Phase::QualityDone.name(), "QualityDone");
119    }
120
121    #[test]
122    fn session_error_display_validation_failed() {
123        let msg = SessionError::ValidationFailed(vec![]).to_string();
124        assert_eq!(msg, "validation failed: 0 error(s)");
125    }
126
127    #[test]
128    fn session_error_display_unknown_id() {
129        let msg = SessionError::UnknownId("ABC".into()).to_string();
130        assert_eq!(msg, "unknown object ID: 'ABC'");
131    }
132
133    #[test]
134    fn session_error_display_no_snapshot_at_time() {
135        let msg = SessionError::NoSnapshotAtTime { requested_t: 42.0 }.to_string();
136        assert!(msg.contains("42"), "got: {msg}");
137    }
138
139    #[test]
140    fn session_error_display_invalid_phase() {
141        let msg = SessionError::InvalidPhase {
142            expected: "Loaded".into(),
143            actual: "Created".into(),
144        }
145        .to_string();
146        assert!(
147            msg.contains("Loaded") && msg.contains("Created"),
148            "got: {msg}"
149        );
150    }
151
152    #[test]
153    fn session_error_display_hydraulic_solve() {
154        let msg = SessionError::HydraulicSolve(HydraulicError::NotConverged).to_string();
155        assert!(msg.contains("hydraulic solver"), "got: {msg}");
156    }
157
158    #[test]
159    fn session_error_display_quality_engine() {
160        let msg = SessionError::QualityEngine(QualityError::ModeNone).to_string();
161        assert!(msg.contains("quality engine"), "got: {msg}");
162    }
163}