cobre_sddp/simulation/
error.rs1use crate::SddpError;
11
12#[derive(Debug, thiserror::Error)]
17pub enum SimulationError {
18 #[error("LP infeasible at scenario {scenario_id}, stage {stage_id}: {solver_message}")]
26 LpInfeasible {
27 scenario_id: u32,
29 stage_id: u32,
31 solver_message: String,
33 },
34
35 #[error("solver error at scenario {scenario_id}, stage {stage_id}: {solver_message}")]
38 SolverError {
39 scenario_id: u32,
41 stage_id: u32,
43 solver_message: String,
45 },
46
47 #[error("I/O error during simulation output: {message}")]
51 IoError {
52 message: String,
54 },
55
56 #[error("policy incompatible with current system: {message}")]
60 PolicyIncompatible {
61 message: String,
63 },
64
65 #[error("simulation output channel closed unexpectedly")]
68 ChannelClosed,
69
70 #[error("stochastic error: {0}")]
72 Stochastic(#[from] cobre_stochastic::StochasticError),
73
74 #[error("invalid simulation configuration: {0}")]
77 InvalidConfiguration(String),
78}
79
80impl From<SimulationError> for SddpError {
81 fn from(err: SimulationError) -> Self {
82 Self::Simulation(err.to_string())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::SimulationError;
89 use crate::SddpError;
90
91 fn assert_send_sync_static<E: std::error::Error + Send + Sync + 'static>() {}
92
93 #[test]
94 fn simulation_error_is_send_sync_static() {
95 assert_send_sync_static::<SimulationError>();
96 }
97
98 #[test]
99 fn simulation_error_lp_infeasible_display() {
100 let err = SimulationError::LpInfeasible {
101 scenario_id: 5,
102 stage_id: 3,
103 solver_message: "primal infeasible".to_string(),
104 };
105 let msg = err.to_string();
106 assert!(msg.contains('5'), "{msg}");
107 assert!(msg.contains('3'), "{msg}");
108 assert!(msg.contains("primal infeasible"), "{msg}");
109 }
110
111 #[test]
112 fn simulation_error_solver_error_display() {
113 let err = SimulationError::SolverError {
114 scenario_id: 10,
115 stage_id: 7,
116 solver_message: "numerical difficulties".to_string(),
117 };
118 let msg = err.to_string();
119 assert!(msg.contains("10"), "{msg}");
120 assert!(msg.contains('7'), "{msg}");
121 assert!(msg.contains("numerical difficulties"), "{msg}");
122 }
123
124 #[test]
125 fn simulation_error_io_error_display() {
126 let err = SimulationError::IoError {
127 message: "disk full".to_string(),
128 };
129 let msg = err.to_string();
130 assert!(msg.contains("disk full"), "{msg}");
131 }
132
133 #[test]
134 fn simulation_error_policy_incompatible_display() {
135 let err = SimulationError::PolicyIncompatible {
136 message: "hydro count mismatch: expected 5, got 6".to_string(),
137 };
138 let msg = err.to_string();
139 assert!(msg.contains("hydro count mismatch"), "{msg}");
140 }
141
142 #[test]
143 fn simulation_error_channel_closed_display() {
144 let err = SimulationError::ChannelClosed;
145 let msg = err.to_string();
146 assert!(!msg.is_empty(), "ChannelClosed display must not be empty");
147 assert!(
148 msg.contains("channel") || msg.contains("closed"),
149 "ChannelClosed display should mention channel or closed: {msg}"
150 );
151 }
152
153 #[test]
154 fn simulation_error_satisfies_std_error_trait() {
155 let variants: Vec<SimulationError> = vec![
156 SimulationError::LpInfeasible {
157 scenario_id: 0,
158 stage_id: 0,
159 solver_message: "infeasible".to_string(),
160 },
161 SimulationError::SolverError {
162 scenario_id: 0,
163 stage_id: 0,
164 solver_message: "error".to_string(),
165 },
166 SimulationError::IoError {
167 message: "disk full".to_string(),
168 },
169 SimulationError::PolicyIncompatible {
170 message: "mismatch".to_string(),
171 },
172 SimulationError::ChannelClosed,
173 ];
174 for err in &variants {
175 let _: &dyn std::error::Error = err;
176 }
177 }
178
179 #[test]
180 fn from_simulation_error_to_sddp_error() {
181 let variants: Vec<SimulationError> = vec![
182 SimulationError::LpInfeasible {
183 scenario_id: 1,
184 stage_id: 2,
185 solver_message: "primal infeasible".to_string(),
186 },
187 SimulationError::SolverError {
188 scenario_id: 3,
189 stage_id: 4,
190 solver_message: "numerical issue".to_string(),
191 },
192 SimulationError::IoError {
193 message: "write failed".to_string(),
194 },
195 SimulationError::PolicyIncompatible {
196 message: "bus count mismatch".to_string(),
197 },
198 SimulationError::ChannelClosed,
199 ];
200
201 for variant in variants {
202 let original_msg = variant.to_string();
203 let sddp_err: SddpError = variant.into();
204 assert!(
205 matches!(sddp_err, SddpError::Simulation(_)),
206 "expected SddpError::Simulation, got {sddp_err:?}"
207 );
208 let sddp_msg = sddp_err.to_string();
210 assert!(
211 sddp_msg.contains(original_msg.as_str()) || sddp_msg.contains("simulation"),
212 "SddpError display '{sddp_msg}' should contain original message or 'simulation'"
213 );
214 }
215 }
216
217 #[test]
218 fn sddp_error_simulation_variant_display() {
219 let err = SddpError::Simulation("test".to_string());
220 let msg = err.to_string();
221 assert!(
222 msg.contains("simulation"),
223 "display must contain 'simulation': {msg}"
224 );
225 assert!(msg.contains("test"), "display must contain 'test': {msg}");
226 }
227}