oxirs_physics/simulation/
result_injection.rs1use crate::error::{PhysicsError, PhysicsResult};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8pub struct ResultInjector {
10 #[allow(dead_code)]
12 store_path: Option<String>,
13
14 enable_provenance: bool,
16}
17
18impl ResultInjector {
19 pub fn new() -> Self {
21 Self {
22 store_path: None,
23 enable_provenance: true,
24 }
25 }
26
27 pub fn with_store(store_path: impl Into<String>) -> Self {
29 Self {
30 store_path: Some(store_path.into()),
31 enable_provenance: true,
32 }
33 }
34
35 pub fn without_provenance(mut self) -> Self {
37 self.enable_provenance = false;
38 self
39 }
40
41 pub async fn inject(&self, result: &SimulationResult) -> PhysicsResult<()> {
43 self.validate_result(result)?;
47
48 tracing::debug!(
50 "Would inject {} state vectors for entity {}",
51 result.state_trajectory.len(),
52 result.entity_iri
53 );
54
55 Ok(())
59 }
60
61 fn validate_result(&self, result: &SimulationResult) -> PhysicsResult<()> {
63 if result.entity_iri.is_empty() {
64 return Err(PhysicsError::ResultInjection(
65 "Entity IRI cannot be empty".to_string(),
66 ));
67 }
68
69 if result.simulation_run_id.is_empty() {
70 return Err(PhysicsError::ResultInjection(
71 "Simulation run ID cannot be empty".to_string(),
72 ));
73 }
74
75 if result.state_trajectory.is_empty() {
76 return Err(PhysicsError::ResultInjection(
77 "State trajectory cannot be empty".to_string(),
78 ));
79 }
80
81 Ok(())
82 }
83
84 #[allow(dead_code)]
86 async fn inject_with_sparql(&self, result: &SimulationResult) -> PhysicsResult<()> {
87 let _update_query = self.generate_state_trajectory_update(result);
89
90 if self.enable_provenance {
92 let _provenance_query = self.generate_provenance_update(result);
93 }
94
95 Err(PhysicsError::ResultInjection(
100 "SPARQL UPDATE injection not yet implemented".to_string(),
101 ))
102 }
103
104 fn generate_state_trajectory_update(&self, result: &SimulationResult) -> String {
106 let mut triples = Vec::new();
107
108 triples.push(format!(
110 "<{}> a phys:SimulationRun .",
111 result.simulation_run_id
112 ));
113 triples.push(format!(
114 "<{}> phys:simulatesEntity <{}>",
115 result.simulation_run_id, result.entity_iri
116 ));
117 triples.push(format!(
118 "<{}> phys:timestamp \"{}\"^^xsd:dateTime .",
119 result.simulation_run_id, result.timestamp
120 ));
121
122 triples.push(format!(
124 "<{}> phys:converged {} .",
125 result.simulation_run_id, result.convergence_info.converged
126 ));
127 triples.push(format!(
128 "<{}> phys:iterations {} .",
129 result.simulation_run_id, result.convergence_info.iterations
130 ));
131 triples.push(format!(
132 "<{}> phys:finalResidual {} .",
133 result.simulation_run_id, result.convergence_info.final_residual
134 ));
135
136 for (idx, state) in result.state_trajectory.iter().enumerate().take(100) {
138 let state_iri = format!("{}#state_{}", result.simulation_run_id, idx);
139 triples.push(format!(
140 "<{}> phys:hasState <{}> .",
141 result.simulation_run_id, state_iri
142 ));
143 triples.push(format!("<{}> phys:time {} .", state_iri, state.time));
144
145 for (key, value) in &state.state {
146 triples.push(format!("<{}> phys:{} {} .", state_iri, key, value));
147 }
148 }
149
150 format!(
151 r#"
152 PREFIX phys: <http://example.org/physics#>
153 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
154
155 INSERT DATA {{
156 {}
157 }}
158 "#,
159 triples.join("\n ")
160 )
161 }
162
163 fn generate_provenance_update(&self, result: &SimulationResult) -> String {
165 format!(
166 r#"
167 PREFIX prov: <http://www.w3.org/ns/prov#>
168 PREFIX phys: <http://example.org/physics#>
169 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
170
171 INSERT DATA {{
172 <{}> prov:wasGeneratedBy <{}> .
173 <{}> a prov:Activity .
174 <{}> prov:startedAtTime "{}"^^xsd:dateTime .
175 <{}> prov:used <{}> .
176 <{}> prov:wasAssociatedWith <{}> .
177 <{}> a prov:SoftwareAgent .
178 <{}> prov:label "{}" .
179 <{}> phys:version "{}" .
180 <{}> phys:parametersHash "{}" .
181 }}
182 "#,
183 result.entity_iri,
184 result.simulation_run_id,
185 result.simulation_run_id,
186 result.simulation_run_id,
187 result.provenance.executed_at,
188 result.simulation_run_id,
189 result.entity_iri,
190 result.simulation_run_id,
191 result.provenance.software,
192 result.provenance.software,
193 result.provenance.software,
194 result.provenance.software,
195 result.provenance.software,
196 result.provenance.version,
197 result.simulation_run_id,
198 result.provenance.parameters_hash,
199 )
200 }
201
202 #[allow(dead_code)]
204 async fn inject_in_batches(
205 &self,
206 _result: &SimulationResult,
207 _batch_size: usize,
208 ) -> PhysicsResult<()> {
209 Ok(())
213 }
214}
215
216impl Default for ResultInjector {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct SimulationResult {
225 pub entity_iri: String,
226 pub simulation_run_id: String,
227 pub timestamp: DateTime<Utc>,
228 pub state_trajectory: Vec<StateVector>,
229 pub derived_quantities: HashMap<String, f64>,
230 pub convergence_info: ConvergenceInfo,
231 pub provenance: SimulationProvenance,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct StateVector {
237 pub time: f64,
238 pub state: HashMap<String, f64>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ConvergenceInfo {
244 pub converged: bool,
245 pub iterations: usize,
246 pub final_residual: f64,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct SimulationProvenance {
252 pub software: String,
253 pub version: String,
254 pub parameters_hash: String,
255 pub executed_at: DateTime<Utc>,
256 pub execution_time_ms: u64,
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn create_test_result() -> SimulationResult {
264 let mut trajectory = Vec::new();
265
266 for i in 0..10 {
267 let mut state = HashMap::new();
268 state.insert("temperature".to_string(), 300.0 + i as f64);
269 trajectory.push(StateVector {
270 time: i as f64 * 10.0,
271 state,
272 });
273 }
274
275 SimulationResult {
276 entity_iri: "urn:example:battery:001".to_string(),
277 simulation_run_id: "run-123".to_string(),
278 timestamp: Utc::now(),
279 state_trajectory: trajectory,
280 derived_quantities: HashMap::new(),
281 convergence_info: ConvergenceInfo {
282 converged: true,
283 iterations: 100,
284 final_residual: 1e-6,
285 },
286 provenance: SimulationProvenance {
287 software: "oxirs-physics".to_string(),
288 version: "0.1.0".to_string(),
289 parameters_hash: "abc123".to_string(),
290 executed_at: Utc::now(),
291 execution_time_ms: 1500,
292 },
293 }
294 }
295
296 #[tokio::test]
297 async fn test_result_injector_basic() {
298 let injector = ResultInjector::new();
299 let result = create_test_result();
300
301 assert!(injector.inject(&result).await.is_ok());
303 }
304
305 #[tokio::test]
306 async fn test_result_injector_with_store() {
307 let injector = ResultInjector::with_store("./test_store");
308 let result = create_test_result();
309
310 assert!(injector.inject(&result).await.is_ok());
311 }
312
313 #[tokio::test]
314 async fn test_result_injector_without_provenance() {
315 let injector = ResultInjector::new().without_provenance();
316 let result = create_test_result();
317
318 assert!(injector.inject(&result).await.is_ok());
319 }
320
321 #[tokio::test]
322 async fn test_validate_result_empty_entity_iri() {
323 let injector = ResultInjector::new();
324 let mut result = create_test_result();
325 result.entity_iri = String::new();
326
327 assert!(injector.inject(&result).await.is_err());
329 }
330
331 #[tokio::test]
332 async fn test_validate_result_empty_run_id() {
333 let injector = ResultInjector::new();
334 let mut result = create_test_result();
335 result.simulation_run_id = String::new();
336
337 assert!(injector.inject(&result).await.is_err());
339 }
340
341 #[tokio::test]
342 async fn test_validate_result_empty_trajectory() {
343 let injector = ResultInjector::new();
344 let mut result = create_test_result();
345 result.state_trajectory.clear();
346
347 assert!(injector.inject(&result).await.is_err());
349 }
350
351 #[test]
352 fn test_generate_state_trajectory_update() {
353 let injector = ResultInjector::new();
354 let result = create_test_result();
355
356 let query = injector.generate_state_trajectory_update(&result);
357
358 assert!(query.contains("INSERT DATA"));
360 assert!(query.contains("phys:SimulationRun"));
361 assert!(query.contains(&result.simulation_run_id));
362 assert!(query.contains(&result.entity_iri));
363 assert!(query.contains("phys:converged"));
364 assert!(query.contains("phys:iterations"));
365 }
366
367 #[test]
368 fn test_generate_provenance_update() {
369 let injector = ResultInjector::new();
370 let result = create_test_result();
371
372 let query = injector.generate_provenance_update(&result);
373
374 assert!(query.contains("prov:wasGeneratedBy"));
376 assert!(query.contains("prov:Activity"));
377 assert!(query.contains("prov:SoftwareAgent"));
378 assert!(query.contains(&result.provenance.software));
379 assert!(query.contains(&result.provenance.version));
380 assert!(query.contains(&result.provenance.parameters_hash));
381 }
382
383 #[test]
384 fn test_simulation_result_serialization() {
385 let result = create_test_result();
386
387 let json = serde_json::to_string(&result).unwrap();
388 let deserialized: SimulationResult = serde_json::from_str(&json).unwrap();
389
390 assert_eq!(deserialized.entity_iri, result.entity_iri);
391 assert_eq!(deserialized.simulation_run_id, result.simulation_run_id);
392 assert_eq!(
393 deserialized.state_trajectory.len(),
394 result.state_trajectory.len()
395 );
396 assert_eq!(
397 deserialized.convergence_info.converged,
398 result.convergence_info.converged
399 );
400 }
401
402 #[test]
403 fn test_state_vector_serialization() {
404 let mut state = HashMap::new();
405 state.insert("temperature".to_string(), 300.0);
406 state.insert("pressure".to_string(), 101325.0);
407
408 let vector = StateVector {
409 time: 10.0,
410 state: state.clone(),
411 };
412
413 let json = serde_json::to_string(&vector).unwrap();
414 let deserialized: StateVector = serde_json::from_str(&json).unwrap();
415
416 assert_eq!(deserialized.time, 10.0);
417 assert_eq!(deserialized.state.len(), 2);
418 assert_eq!(deserialized.state.get("temperature"), Some(&300.0));
419 }
420
421 #[test]
422 fn test_convergence_info() {
423 let info = ConvergenceInfo {
424 converged: true,
425 iterations: 150,
426 final_residual: 5e-7,
427 };
428
429 assert!(info.converged);
430 assert_eq!(info.iterations, 150);
431 assert!(info.final_residual < 1e-6);
432 }
433
434 #[test]
435 fn test_provenance_tracking() {
436 let prov = SimulationProvenance {
437 software: "oxirs-physics".to_string(),
438 version: "0.1.0".to_string(),
439 parameters_hash: "def456".to_string(),
440 executed_at: Utc::now(),
441 execution_time_ms: 2500,
442 };
443
444 assert_eq!(prov.software, "oxirs-physics");
445 assert_eq!(prov.version, "0.1.0");
446 assert_eq!(prov.execution_time_ms, 2500);
447 }
448}