oxirs_physics/simulation/
result_injection.rs

1//! Result Injection back to RDF
2
3use crate::error::{PhysicsError, PhysicsResult};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Writes simulation results back to RDF graph
9pub struct ResultInjector {
10    /// Optional RDF store connection (for future SPARQL UPDATE)
11    #[allow(dead_code)]
12    store_path: Option<String>,
13
14    /// Enable provenance tracking with W3C PROV ontology
15    enable_provenance: bool,
16}
17
18impl ResultInjector {
19    /// Create a new result injector
20    pub fn new() -> Self {
21        Self {
22            store_path: None,
23            enable_provenance: true,
24        }
25    }
26
27    /// Create a result injector with RDF store connection
28    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    /// Disable provenance tracking
36    pub fn without_provenance(mut self) -> Self {
37        self.enable_provenance = false;
38        self
39    }
40
41    /// Inject simulation results into RDF graph
42    pub async fn inject(&self, result: &SimulationResult) -> PhysicsResult<()> {
43        // For now, just validate the result structure
44        // Future implementation will use SPARQL UPDATE to insert triples
45
46        self.validate_result(result)?;
47
48        // Mock injection - log what would be inserted
49        tracing::debug!(
50            "Would inject {} state vectors for entity {}",
51            result.state_trajectory.len(),
52            result.entity_iri
53        );
54
55        // Future: Execute SPARQL UPDATE
56        // self.inject_with_sparql(result).await?;
57
58        Ok(())
59    }
60
61    /// Validate result structure before injection
62    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    /// Inject results using SPARQL UPDATE (future implementation)
85    #[allow(dead_code)]
86    async fn inject_with_sparql(&self, result: &SimulationResult) -> PhysicsResult<()> {
87        // Generate SPARQL UPDATE query for state trajectory
88        let _update_query = self.generate_state_trajectory_update(result);
89
90        // Generate provenance triples if enabled
91        if self.enable_provenance {
92            let _provenance_query = self.generate_provenance_update(result);
93        }
94
95        // TODO: Execute SPARQL UPDATE using oxirs-core::RdfStore
96        // TODO: Handle batch inserts for large trajectories (chunk by 1000 states)
97        // TODO: Use transactions for atomicity
98
99        Err(PhysicsError::ResultInjection(
100            "SPARQL UPDATE injection not yet implemented".to_string(),
101        ))
102    }
103
104    /// Generate SPARQL UPDATE for state trajectory
105    fn generate_state_trajectory_update(&self, result: &SimulationResult) -> String {
106        let mut triples = Vec::new();
107
108        // Create simulation run resource
109        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        // Add convergence info
123        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        // Add state trajectory (sample to avoid huge queries)
137        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    /// Generate SPARQL UPDATE for provenance (W3C PROV ontology)
164    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    /// Batch insert for large trajectories (future implementation)
203    #[allow(dead_code)]
204    async fn inject_in_batches(
205        &self,
206        _result: &SimulationResult,
207        _batch_size: usize,
208    ) -> PhysicsResult<()> {
209        // TODO: Split state_trajectory into chunks of batch_size
210        // TODO: Generate SPARQL UPDATE for each batch
211        // TODO: Execute batches sequentially or in parallel
212        Ok(())
213    }
214}
215
216impl Default for ResultInjector {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222/// Simulation Result
223#[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/// State vector at a time point
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct StateVector {
237    pub time: f64,
238    pub state: HashMap<String, f64>,
239}
240
241/// Convergence information
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ConvergenceInfo {
244    pub converged: bool,
245    pub iterations: usize,
246    pub final_residual: f64,
247}
248
249/// Simulation provenance
250#[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        // Should succeed with valid result
302        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        // Should fail with empty entity IRI
328        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        // Should fail with empty run ID
338        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        // Should fail with empty trajectory
348        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        // Check that query contains key elements
359        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        // Check that query contains W3C PROV elements
375        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}