Skip to main content

oxirs_physics/simulation/
result_injection.rs

1//! Result Injection back to RDF
2//!
3//! Writes simulation results back to RDF graph using SPARQL UPDATE.
4//! Supports provenance tracking, timestamps, and batch operations.
5
6use crate::error::{PhysicsError, PhysicsResult};
7use chrono::{DateTime, Utc};
8use oxirs_core::model::NamedNode;
9use oxirs_core::query::{UpdateExecutor, UpdateParser};
10use oxirs_core::rdf_store::RdfStore;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14use uuid::Uuid;
15
16/// Writes simulation results back to RDF graph
17pub struct ResultInjector {
18    /// RDF store for updates
19    store: Option<Arc<RdfStore>>,
20
21    /// Enable provenance tracking with W3C PROV ontology
22    enable_provenance: bool,
23
24    /// Configuration
25    config: InjectionConfig,
26}
27
28/// Injection configuration
29#[derive(Debug, Clone)]
30pub struct InjectionConfig {
31    /// Physics namespace prefix
32    pub physics_prefix: String,
33    /// PROV namespace prefix
34    pub prov_prefix: String,
35    /// Batch size for large trajectories
36    pub batch_size: usize,
37    /// Use transactions
38    pub use_transactions: bool,
39}
40
41impl Default for InjectionConfig {
42    fn default() -> Self {
43        Self {
44            physics_prefix: "http://oxirs.org/physics#".to_string(),
45            prov_prefix: "http://www.w3.org/ns/prov#".to_string(),
46            batch_size: 1000,
47            use_transactions: true,
48        }
49    }
50}
51
52impl ResultInjector {
53    /// Create a new result injector without store (for testing)
54    pub fn new() -> Self {
55        Self {
56            store: None,
57            enable_provenance: true,
58            config: InjectionConfig::default(),
59        }
60    }
61
62    /// Create a result injector with RDF store
63    pub fn with_store(store: Arc<RdfStore>) -> Self {
64        Self {
65            store: Some(store),
66            enable_provenance: true,
67            config: InjectionConfig::default(),
68        }
69    }
70
71    /// Set configuration
72    pub fn with_config(mut self, config: InjectionConfig) -> Self {
73        self.config = config;
74        self
75    }
76
77    /// Disable provenance tracking
78    pub fn without_provenance(mut self) -> Self {
79        self.enable_provenance = false;
80        self
81    }
82
83    /// Inject simulation results into RDF graph
84    pub async fn inject(&self, result: &SimulationResult) -> PhysicsResult<()> {
85        // Validate result structure
86        self.validate_result(result)?;
87
88        if let Some(ref store) = self.store {
89            // Inject with SPARQL UPDATE
90            self.inject_with_sparql(store, result).await?;
91        } else {
92            // Mock injection - just log
93            tracing::debug!(
94                "Would inject {} state vectors for entity {}",
95                result.state_trajectory.len(),
96                result.entity_iri
97            );
98        }
99
100        Ok(())
101    }
102
103    /// Validate result structure before injection
104    fn validate_result(&self, result: &SimulationResult) -> PhysicsResult<()> {
105        if result.entity_iri.is_empty() {
106            return Err(PhysicsError::ResultInjection(
107                "Entity IRI cannot be empty".to_string(),
108            ));
109        }
110
111        if result.simulation_run_id.is_empty() {
112            return Err(PhysicsError::ResultInjection(
113                "Simulation run ID cannot be empty".to_string(),
114            ));
115        }
116
117        if result.state_trajectory.is_empty() {
118            return Err(PhysicsError::ResultInjection(
119                "State trajectory cannot be empty".to_string(),
120            ));
121        }
122
123        Ok(())
124    }
125
126    /// Inject results using SPARQL UPDATE
127    async fn inject_with_sparql(
128        &self,
129        store: &RdfStore,
130        result: &SimulationResult,
131    ) -> PhysicsResult<()> {
132        // Generate SPARQL UPDATE for metadata
133        let metadata_update = self.generate_metadata_update(result);
134
135        // Execute metadata update
136        self.execute_update(store, &metadata_update).await?;
137
138        // Inject state trajectory in batches
139        if result.state_trajectory.len() > self.config.batch_size {
140            self.inject_in_batches(store, result).await?;
141        } else {
142            let trajectory_update = self.generate_state_trajectory_update(result);
143            self.execute_update(store, &trajectory_update).await?;
144        }
145
146        // Generate provenance if enabled
147        if self.enable_provenance {
148            let provenance_update = self.generate_provenance_update(result);
149            self.execute_update(store, &provenance_update).await?;
150        }
151
152        Ok(())
153    }
154
155    /// Execute SPARQL UPDATE
156    async fn execute_update(&self, store: &RdfStore, update_query: &str) -> PhysicsResult<()> {
157        let parser = UpdateParser::new();
158        let parsed = parser
159            .parse(update_query)
160            .map_err(|e| PhysicsError::ResultInjection(format!("Parse failed: {e}")))?;
161        let executor = UpdateExecutor::new(store);
162        executor
163            .execute(&parsed)
164            .map_err(|e| PhysicsError::ResultInjection(format!("Execute failed: {e}")))?;
165        Ok(())
166    }
167
168    /// Generate SPARQL UPDATE for simulation metadata
169    fn generate_metadata_update(&self, result: &SimulationResult) -> String {
170        let phys = &self.config.physics_prefix;
171
172        format!(
173            r#"
174            PREFIX phys: <{phys}>
175            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
176
177            INSERT DATA {{
178                <{run_id}> a phys:SimulationRun .
179                <{run_id}> phys:simulatesEntity <{entity}> .
180                <{run_id}> phys:timestamp "{timestamp}"^^xsd:dateTime .
181                <{run_id}> phys:converged "{converged}"^^xsd:boolean .
182                <{run_id}> phys:iterations "{iterations}"^^xsd:integer .
183                <{run_id}> phys:finalResidual "{residual}"^^xsd:double .
184            }}
185            "#,
186            phys = phys,
187            run_id = result.simulation_run_id,
188            entity = result.entity_iri,
189            timestamp = result.timestamp.to_rfc3339(),
190            converged = result.convergence_info.converged,
191            iterations = result.convergence_info.iterations,
192            residual = result.convergence_info.final_residual,
193        )
194    }
195
196    /// Generate SPARQL UPDATE for state trajectory
197    fn generate_state_trajectory_update(&self, result: &SimulationResult) -> String {
198        let phys = &self.config.physics_prefix;
199        let mut triples = Vec::new();
200
201        // Limit to first 100 states to avoid huge queries (rest handled by batches)
202        let states_to_insert = result.state_trajectory.iter().take(100);
203
204        for (idx, state) in states_to_insert.enumerate() {
205            let state_id = format!("{}#state_{}", result.simulation_run_id, idx);
206
207            triples.push(format!(
208                "<{run_id}> phys:hasState <{state_id}> .",
209                run_id = result.simulation_run_id,
210                state_id = state_id
211            ));
212            triples.push(format!(
213                "<{state_id}> phys:time \"{time}\"^^xsd:double .",
214                state_id = state_id,
215                time = state.time
216            ));
217
218            for (key, value) in &state.state {
219                triples.push(format!(
220                    "<{state_id}> phys:{key} \"{value}\"^^xsd:double .",
221                    state_id = state_id,
222                    key = key,
223                    value = value
224                ));
225            }
226        }
227
228        // Add derived quantities
229        for (key, value) in &result.derived_quantities {
230            triples.push(format!(
231                "<{run_id}> phys:{key} \"{value}\"^^xsd:double .",
232                run_id = result.simulation_run_id,
233                key = key,
234                value = value
235            ));
236        }
237
238        format!(
239            r#"
240            PREFIX phys: <{phys}>
241            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
242
243            INSERT DATA {{
244                {triples}
245            }}
246            "#,
247            phys = phys,
248            triples = triples.join("\n                ")
249        )
250    }
251
252    /// Generate SPARQL UPDATE for provenance (W3C PROV ontology)
253    fn generate_provenance_update(&self, result: &SimulationResult) -> String {
254        let prov = &self.config.prov_prefix;
255        let phys = &self.config.physics_prefix;
256
257        format!(
258            r#"
259            PREFIX prov: <{prov}>
260            PREFIX phys: <{phys}>
261            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
262
263            INSERT DATA {{
264                <{entity}> prov:wasGeneratedBy <{run_id}> .
265                <{run_id}> a prov:Activity .
266                <{run_id}> prov:startedAtTime "{executed_at}"^^xsd:dateTime .
267                <{run_id}> prov:used <{entity}> .
268                <{run_id}> prov:wasAssociatedWith <{software_agent}> .
269                <{software_agent}> a prov:SoftwareAgent .
270                <{software_agent}> prov:label "{software}"^^xsd:string .
271                <{software_agent}> phys:version "{version}"^^xsd:string .
272                <{run_id}> phys:parametersHash "{params_hash}"^^xsd:string .
273                <{run_id}> phys:executionTimeMs "{exec_time}"^^xsd:integer .
274            }}
275            "#,
276            prov = prov,
277            phys = phys,
278            entity = result.entity_iri,
279            run_id = result.simulation_run_id,
280            executed_at = result.provenance.executed_at.to_rfc3339(),
281            software_agent = format!("urn:agent:{}", result.provenance.software),
282            software = result.provenance.software,
283            version = result.provenance.version,
284            params_hash = result.provenance.parameters_hash,
285            exec_time = result.provenance.execution_time_ms,
286        )
287    }
288
289    /// Batch insert for large trajectories
290    async fn inject_in_batches(
291        &self,
292        store: &RdfStore,
293        result: &SimulationResult,
294    ) -> PhysicsResult<()> {
295        let phys = &self.config.physics_prefix;
296
297        for (batch_idx, chunk) in result
298            .state_trajectory
299            .chunks(self.config.batch_size)
300            .enumerate()
301        {
302            let mut triples = Vec::new();
303
304            for (idx_in_chunk, state) in chunk.iter().enumerate() {
305                let global_idx = batch_idx * self.config.batch_size + idx_in_chunk;
306                let state_id = format!("{}#state_{}", result.simulation_run_id, global_idx);
307
308                triples.push(format!(
309                    "<{run_id}> phys:hasState <{state_id}> .",
310                    run_id = result.simulation_run_id,
311                    state_id = state_id
312                ));
313                triples.push(format!(
314                    "<{state_id}> phys:time \"{time}\"^^xsd:double .",
315                    state_id = state_id,
316                    time = state.time
317                ));
318
319                for (key, value) in &state.state {
320                    triples.push(format!(
321                        "<{state_id}> phys:{key} \"{value}\"^^xsd:double .",
322                        state_id = state_id,
323                        key = key,
324                        value = value
325                    ));
326                }
327            }
328
329            let batch_update = format!(
330                r#"
331                PREFIX phys: <{phys}>
332                PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
333
334                INSERT DATA {{
335                    {triples}
336                }}
337                "#,
338                phys = phys,
339                triples = triples.join("\n                    ")
340            );
341
342            self.execute_update(store, &batch_update).await?;
343
344            tracing::debug!("Injected batch {} ({} states)", batch_idx + 1, chunk.len());
345        }
346
347        Ok(())
348    }
349
350    /// Create result node IRI
351    pub fn create_result_node(
352        &self,
353        _entity: &NamedNode,
354        property: &str,
355    ) -> PhysicsResult<NamedNode> {
356        let result_id = Uuid::new_v4();
357        let result_iri = format!(
358            "{}result_{}_{}",
359            self.config.physics_prefix, property, result_id
360        );
361
362        NamedNode::new(&result_iri)
363            .map_err(|e| PhysicsError::ResultInjection(format!("Invalid result IRI: {}", e)))
364    }
365
366    /// Write timestamped value
367    pub async fn write_timestamped_value(
368        &self,
369        store: &RdfStore,
370        result_node: &NamedNode,
371        value: &ResultValue,
372    ) -> PhysicsResult<()> {
373        let timestamp = Utc::now();
374        let phys = &self.config.physics_prefix;
375
376        let update = match &value.value {
377            ResultData::Scalar(v) => {
378                format!(
379                    r#"
380                    PREFIX phys: <{phys}>
381                    PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
382
383                    INSERT DATA {{
384                        <{node}> phys:property "{property}"^^xsd:string .
385                        <{node}> phys:value "{value}"^^xsd:double .
386                        <{node}> phys:timestamp "{timestamp}"^^xsd:dateTime .
387                    }}
388                    "#,
389                    phys = phys,
390                    node = result_node.as_str(),
391                    property = value.property,
392                    value = v,
393                    timestamp = timestamp.to_rfc3339(),
394                )
395            }
396            ResultData::Vector(vec) => {
397                let mut triples = Vec::new();
398                triples.push(format!(
399                    "<{node}> phys:property \"{property}\"^^xsd:string .",
400                    node = result_node.as_str(),
401                    property = value.property
402                ));
403                triples.push(format!(
404                    "<{node}> phys:timestamp \"{timestamp}\"^^xsd:dateTime .",
405                    node = result_node.as_str(),
406                    timestamp = timestamp.to_rfc3339()
407                ));
408
409                for (i, v) in vec.iter().enumerate() {
410                    triples.push(format!(
411                        "<{node}> phys:component{i} \"{v}\"^^xsd:double .",
412                        node = result_node.as_str(),
413                        i = i,
414                        v = v
415                    ));
416                }
417
418                format!(
419                    r#"
420                    PREFIX phys: <{phys}>
421                    PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
422
423                    INSERT DATA {{
424                        {triples}
425                    }}
426                    "#,
427                    phys = phys,
428                    triples = triples.join("\n                        ")
429                )
430            }
431            ResultData::Tensor(tensor) => {
432                let mut triples = Vec::new();
433                triples.push(format!(
434                    "<{node}> phys:property \"{property}\"^^xsd:string .",
435                    node = result_node.as_str(),
436                    property = value.property
437                ));
438
439                for (i, row) in tensor.iter().enumerate() {
440                    for (j, v) in row.iter().enumerate() {
441                        triples.push(format!(
442                            "<{node}> phys:tensor_{i}_{j} \"{v}\"^^xsd:double .",
443                            node = result_node.as_str(),
444                            i = i,
445                            j = j,
446                            v = v
447                        ));
448                    }
449                }
450
451                format!(
452                    r#"
453                    PREFIX phys: <{phys}>
454                    PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
455
456                    INSERT DATA {{
457                        {triples}
458                    }}
459                    "#,
460                    phys = phys,
461                    triples = triples.join("\n                        ")
462                )
463            }
464            ResultData::TimeSeries(series) => {
465                let mut triples = Vec::new();
466                triples.push(format!(
467                    "<{node}> phys:property \"{property}\"^^xsd:string .",
468                    node = result_node.as_str(),
469                    property = value.property
470                ));
471
472                for (i, (time, value)) in series.iter().enumerate() {
473                    let point_id = format!("{}#point_{}", result_node.as_str(), i);
474                    triples.push(format!(
475                        "<{node}> phys:hasPoint <{point_id}> .",
476                        node = result_node.as_str(),
477                        point_id = point_id
478                    ));
479                    triples.push(format!(
480                        "<{point_id}> phys:time \"{time}\"^^xsd:double .",
481                        point_id = point_id,
482                        time = time
483                    ));
484                    triples.push(format!(
485                        "<{point_id}> phys:value \"{value}\"^^xsd:double .",
486                        point_id = point_id,
487                        value = value
488                    ));
489                }
490
491                format!(
492                    r#"
493                    PREFIX phys: <{phys}>
494                    PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
495
496                    INSERT DATA {{
497                        {triples}
498                    }}
499                    "#,
500                    phys = phys,
501                    triples = triples.join("\n                        ")
502                )
503            }
504        };
505
506        self.execute_update(store, &update).await
507    }
508
509    /// Add provenance metadata
510    pub async fn add_provenance(
511        &self,
512        store: &RdfStore,
513        result_node: &NamedNode,
514        provenance: &ProvenanceInfo,
515    ) -> PhysicsResult<()> {
516        let prov = &self.config.prov_prefix;
517
518        let update = format!(
519            r#"
520            PREFIX prov: <{prov}>
521            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
522
523            INSERT DATA {{
524                <{node}> prov:wasGeneratedBy <{activity}> .
525                <{activity}> a prov:Activity .
526                <{activity}> prov:startedAtTime "{timestamp}"^^xsd:dateTime .
527                <{activity}> prov:wasAssociatedWith <{agent}> .
528                <{agent}> a prov:SoftwareAgent .
529                <{agent}> prov:label "{software}"^^xsd:string .
530            }}
531            "#,
532            prov = prov,
533            node = result_node.as_str(),
534            activity = provenance.activity_id,
535            timestamp = provenance.timestamp.to_rfc3339(),
536            agent = provenance.agent_id,
537            software = provenance.software,
538        );
539
540        self.execute_update(store, &update).await
541    }
542
543    /// Begin transaction
544    pub fn begin_transaction(&self) -> PhysicsResult<Transaction> {
545        Ok(Transaction {
546            id: Uuid::new_v4().to_string(),
547            updates: Vec::new(),
548        })
549    }
550
551    /// Commit transaction
552    pub async fn commit_transaction(&self, store: &RdfStore, tx: Transaction) -> PhysicsResult<()> {
553        for update in tx.updates {
554            self.execute_update(store, &update).await?;
555        }
556        Ok(())
557    }
558}
559
560impl Default for ResultInjector {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566/// Transaction for atomic writes
567#[derive(Debug, Clone)]
568pub struct Transaction {
569    pub id: String,
570    pub updates: Vec<String>,
571}
572
573/// Provenance information
574#[derive(Debug, Clone)]
575pub struct ProvenanceInfo {
576    pub activity_id: String,
577    pub agent_id: String,
578    pub software: String,
579    pub timestamp: DateTime<Utc>,
580}
581
582/// Result value
583#[derive(Debug, Clone)]
584pub struct ResultValue {
585    pub property: String,
586    pub value: ResultData,
587}
588
589/// Result data types
590#[derive(Debug, Clone)]
591pub enum ResultData {
592    Scalar(f64),
593    Vector(Vec<f64>),
594    Tensor(Vec<Vec<f64>>),
595    TimeSeries(Vec<(f64, f64)>),
596}
597
598/// Simulation Result
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct SimulationResult {
601    pub entity_iri: String,
602    pub simulation_run_id: String,
603    pub timestamp: DateTime<Utc>,
604    pub state_trajectory: Vec<StateVector>,
605    pub derived_quantities: HashMap<String, f64>,
606    pub convergence_info: ConvergenceInfo,
607    pub provenance: SimulationProvenance,
608}
609
610/// State vector at a time point
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct StateVector {
613    pub time: f64,
614    pub state: HashMap<String, f64>,
615}
616
617/// Convergence information
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct ConvergenceInfo {
620    pub converged: bool,
621    pub iterations: usize,
622    pub final_residual: f64,
623}
624
625/// Simulation provenance
626#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct SimulationProvenance {
628    pub software: String,
629    pub version: String,
630    pub parameters_hash: String,
631    pub executed_at: DateTime<Utc>,
632    pub execution_time_ms: u64,
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    // `find_quads` is provided by the `Store` trait, which must be in scope.
639    use oxirs_core::Store;
640
641    fn create_test_result() -> SimulationResult {
642        let mut trajectory = Vec::new();
643
644        for i in 0..10 {
645            let mut state = HashMap::new();
646            state.insert("temperature".to_string(), 300.0 + i as f64);
647            trajectory.push(StateVector {
648                time: i as f64 * 10.0,
649                state,
650            });
651        }
652
653        SimulationResult {
654            entity_iri: "urn:example:battery:001".to_string(),
655            simulation_run_id: "run-123".to_string(),
656            timestamp: Utc::now(),
657            state_trajectory: trajectory,
658            derived_quantities: HashMap::new(),
659            convergence_info: ConvergenceInfo {
660                converged: true,
661                iterations: 100,
662                final_residual: 1e-6,
663            },
664            provenance: SimulationProvenance {
665                software: "oxirs-physics".to_string(),
666                version: "0.1.0".to_string(),
667                parameters_hash: "abc123".to_string(),
668                executed_at: Utc::now(),
669                execution_time_ms: 1500,
670            },
671        }
672    }
673
674    #[tokio::test]
675    async fn test_result_injector_basic() {
676        let injector = ResultInjector::new();
677        let result = create_test_result();
678
679        // Should succeed with valid result
680        assert!(injector.inject(&result).await.is_ok());
681    }
682
683    #[tokio::test]
684    async fn test_result_injector_without_provenance() {
685        let injector = ResultInjector::new().without_provenance();
686        let result = create_test_result();
687
688        assert!(injector.inject(&result).await.is_ok());
689    }
690
691    #[tokio::test]
692    async fn test_validate_result_empty_entity_iri() {
693        let injector = ResultInjector::new();
694        let mut result = create_test_result();
695        result.entity_iri = String::new();
696
697        // Should fail with empty entity IRI
698        assert!(injector.inject(&result).await.is_err());
699    }
700
701    #[tokio::test]
702    async fn test_validate_result_empty_run_id() {
703        let injector = ResultInjector::new();
704        let mut result = create_test_result();
705        result.simulation_run_id = String::new();
706
707        // Should fail with empty run ID
708        assert!(injector.inject(&result).await.is_err());
709    }
710
711    #[tokio::test]
712    async fn test_validate_result_empty_trajectory() {
713        let injector = ResultInjector::new();
714        let mut result = create_test_result();
715        result.state_trajectory.clear();
716
717        // Should fail with empty trajectory
718        assert!(injector.inject(&result).await.is_err());
719    }
720
721    #[test]
722    fn test_generate_metadata_update() {
723        let injector = ResultInjector::new();
724        let result = create_test_result();
725
726        let query = injector.generate_metadata_update(&result);
727
728        // Check that query contains key elements
729        assert!(query.contains("INSERT DATA"));
730        assert!(query.contains("phys:SimulationRun"));
731        assert!(query.contains(&result.simulation_run_id));
732        assert!(query.contains(&result.entity_iri));
733        assert!(query.contains("phys:converged"));
734        assert!(query.contains("phys:iterations"));
735    }
736
737    #[test]
738    fn test_generate_state_trajectory_update() {
739        let injector = ResultInjector::new();
740        let result = create_test_result();
741
742        let query = injector.generate_state_trajectory_update(&result);
743
744        // Check that query contains state information
745        assert!(query.contains("INSERT DATA"));
746        assert!(query.contains("phys:hasState"));
747        assert!(query.contains("phys:time"));
748        assert!(query.contains(&result.simulation_run_id));
749    }
750
751    #[test]
752    fn test_generate_provenance_update() {
753        let injector = ResultInjector::new();
754        let result = create_test_result();
755
756        let query = injector.generate_provenance_update(&result);
757
758        // Check that query contains W3C PROV elements
759        assert!(query.contains("prov:wasGeneratedBy"));
760        assert!(query.contains("prov:Activity"));
761        assert!(query.contains("prov:SoftwareAgent"));
762        assert!(query.contains(&result.provenance.software));
763        assert!(query.contains(&result.provenance.version));
764        assert!(query.contains(&result.provenance.parameters_hash));
765    }
766
767    #[test]
768    fn test_simulation_result_serialization() {
769        let result = create_test_result();
770
771        let json = serde_json::to_string(&result).expect("Failed to serialize");
772        let deserialized: SimulationResult =
773            serde_json::from_str(&json).expect("Failed to deserialize");
774
775        assert_eq!(deserialized.entity_iri, result.entity_iri);
776        assert_eq!(deserialized.simulation_run_id, result.simulation_run_id);
777        assert_eq!(
778            deserialized.state_trajectory.len(),
779            result.state_trajectory.len()
780        );
781        assert_eq!(
782            deserialized.convergence_info.converged,
783            result.convergence_info.converged
784        );
785    }
786
787    #[test]
788    fn test_state_vector_serialization() {
789        let mut state = HashMap::new();
790        state.insert("temperature".to_string(), 300.0);
791        state.insert("pressure".to_string(), 101325.0);
792
793        let vector = StateVector {
794            time: 10.0,
795            state: state.clone(),
796        };
797
798        let json = serde_json::to_string(&vector).expect("Failed to serialize");
799        let deserialized: StateVector = serde_json::from_str(&json).expect("Failed to deserialize");
800
801        assert_eq!(deserialized.time, 10.0);
802        assert_eq!(deserialized.state.len(), 2);
803        assert_eq!(deserialized.state.get("temperature"), Some(&300.0));
804    }
805
806    #[test]
807    fn test_convergence_info() {
808        let info = ConvergenceInfo {
809            converged: true,
810            iterations: 150,
811            final_residual: 5e-7,
812        };
813
814        assert!(info.converged);
815        assert_eq!(info.iterations, 150);
816        assert!(info.final_residual < 1e-6);
817    }
818
819    #[test]
820    fn test_provenance_tracking() {
821        let prov = SimulationProvenance {
822            software: "oxirs-physics".to_string(),
823            version: "0.1.0".to_string(),
824            parameters_hash: "def456".to_string(),
825            executed_at: Utc::now(),
826            execution_time_ms: 2500,
827        };
828
829        assert_eq!(prov.software, "oxirs-physics");
830        assert_eq!(prov.version, "0.1.0");
831        assert_eq!(prov.execution_time_ms, 2500);
832    }
833
834    #[test]
835    fn test_injection_config() {
836        let config = InjectionConfig {
837            physics_prefix: "http://example.org/phys#".to_string(),
838            prov_prefix: "http://example.org/prov#".to_string(),
839            batch_size: 500,
840            use_transactions: false,
841        };
842
843        assert_eq!(config.physics_prefix, "http://example.org/phys#");
844        assert_eq!(config.batch_size, 500);
845        assert!(!config.use_transactions);
846    }
847
848    #[test]
849    fn test_create_result_node() {
850        let injector = ResultInjector::new();
851        let entity = NamedNode::new("http://example.org/entity1").expect("Failed to create node");
852
853        let result_node = injector
854            .create_result_node(&entity, "displacement")
855            .expect("Failed to create result node");
856
857        assert!(result_node.as_str().contains("displacement"));
858        assert!(result_node
859            .as_str()
860            .starts_with("http://oxirs.org/physics#result_"));
861    }
862
863    #[test]
864    fn test_transaction() {
865        let injector = ResultInjector::new();
866        let tx = injector
867            .begin_transaction()
868            .expect("Failed to begin transaction");
869
870        assert!(!tx.id.is_empty());
871        assert!(tx.updates.is_empty());
872    }
873
874    #[tokio::test]
875    async fn test_execute_update_insert_data() {
876        let store = RdfStore::default();
877        let injector = ResultInjector::new();
878        let update =
879            "INSERT DATA { <http://example.org/s> <http://example.org/p> <http://example.org/o> }";
880        let result = injector.execute_update(&store, update).await;
881        assert!(
882            result.is_ok(),
883            "execute_update should succeed: {:?}",
884            result
885        );
886        // Verify the triple landed
887        let quads = store
888            .find_quads(None, None, None, None)
889            .expect("find_quads failed");
890        assert!(
891            !quads.is_empty(),
892            "Store should have quads after INSERT DATA"
893        );
894    }
895
896    #[tokio::test]
897    async fn test_execute_update_parse_error() {
898        let store = RdfStore::default();
899        let injector = ResultInjector::new();
900        let bad_update = "NOT VALID SPARQL UPDATE";
901        let result = injector.execute_update(&store, bad_update).await;
902        assert!(result.is_err(), "Should fail on invalid SPARQL");
903    }
904}