Skip to main content

oxirs_physics/simulation/
parameter_extraction.rs

1//! Parameter Extraction from RDF and SAMM
2//!
3//! Extracts physical simulation parameters from RDF graphs using SPARQL queries.
4//! Supports unit conversion, default values, and SAMM Aspect Model integration.
5
6use crate::error::{PhysicsError, PhysicsResult};
7use oxirs_core::model::{NamedNode, Term};
8use oxirs_core::rdf_store::{QueryResults, RdfStore};
9use scirs2_core::units::UnitRegistry;
10use scirs2_core::validation::check_finite;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14
15/// Extracts simulation parameters from RDF graphs and SAMM Aspect Models
16pub struct ParameterExtractor {
17    /// RDF store for querying
18    store: Option<Arc<RdfStore>>,
19    /// Unit conversion registry
20    unit_registry: UnitRegistry,
21    /// Configuration
22    config: ExtractionConfig,
23}
24
25/// Extraction configuration
26#[derive(Debug, Clone)]
27pub struct ExtractionConfig {
28    /// Use fallback defaults for missing properties
29    pub use_defaults: bool,
30    /// Physics namespace prefix
31    pub physics_prefix: String,
32    /// Validate extracted values
33    pub validate: bool,
34}
35
36impl Default for ExtractionConfig {
37    fn default() -> Self {
38        Self {
39            use_defaults: true,
40            physics_prefix: "http://oxirs.org/physics#".to_string(),
41            validate: true,
42        }
43    }
44}
45
46impl ParameterExtractor {
47    /// Create a new parameter extractor without store (for testing)
48    pub fn new() -> Self {
49        Self {
50            store: None,
51            unit_registry: UnitRegistry::new(),
52            config: ExtractionConfig::default(),
53        }
54    }
55
56    /// Create a parameter extractor with RDF store
57    pub fn with_store(store: Arc<RdfStore>) -> Self {
58        Self {
59            store: Some(store),
60            unit_registry: UnitRegistry::new(),
61            config: ExtractionConfig::default(),
62        }
63    }
64
65    /// Set configuration
66    pub fn with_config(mut self, config: ExtractionConfig) -> Self {
67        self.config = config;
68        self
69    }
70
71    /// Extract simulation parameters from RDF
72    pub async fn extract(
73        &self,
74        entity_iri: &str,
75        simulation_type: &str,
76    ) -> PhysicsResult<SimulationParameters> {
77        if let Some(ref store) = self.store {
78            self.extract_from_rdf(store, entity_iri, simulation_type)
79                .await
80        } else {
81            // Fallback to mock for testing
82            self.extract_mock_parameters(entity_iri, simulation_type)
83                .await
84        }
85    }
86
87    /// Extract entity from RDF with all properties
88    pub async fn extract_entity(
89        &self,
90        store: &RdfStore,
91        entity_uri: &str,
92    ) -> PhysicsResult<PhysicalEntity> {
93        let entity_node = NamedNode::new(entity_uri)
94            .map_err(|e| PhysicsError::ParameterExtraction(format!("Invalid IRI: {}", e)))?;
95
96        // Query all properties for this entity
97        let query = format!(
98            r#"
99            PREFIX phys: <{prefix}>
100            PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
101            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
102
103            SELECT ?property ?value ?unit WHERE {{
104                <{entity}> ?property ?value .
105                OPTIONAL {{ ?value phys:unit ?unit }}
106            }}
107            "#,
108            prefix = self.config.physics_prefix,
109            entity = entity_uri
110        );
111
112        let results = store
113            .query(&query)
114            .map_err(|e| PhysicsError::RdfQuery(format!("Query failed: {}", e)))?;
115
116        let mut properties = HashMap::new();
117
118        // Parse query results
119        if let QueryResults::Bindings(ref bindings) = results.results() {
120            for binding in bindings {
121                if let (Some(Term::NamedNode(prop_node)), Some(val_term)) =
122                    (binding.get("property"), binding.get("value"))
123                {
124                    let prop_name = prop_node
125                        .as_str()
126                        .split('#')
127                        .next_back()
128                        .or_else(|| prop_node.as_str().split('/').next_back())
129                        .unwrap_or("unknown");
130
131                    let unit = binding
132                        .get("unit")
133                        .and_then(|t| {
134                            if let Term::Literal(lit) = t {
135                                Some(lit.value().to_string())
136                            } else {
137                                None
138                            }
139                        })
140                        .unwrap_or_else(|| "dimensionless".to_string());
141
142                    if let Some(physical_value) = self.parse_value(val_term, &unit)? {
143                        properties.insert(prop_name.to_string(), physical_value);
144                    }
145                }
146            }
147        }
148
149        // Extract relationships
150        let relationships = self.extract_relationships(store, &entity_node).await?;
151
152        Ok(PhysicalEntity {
153            uri: entity_node,
154            properties,
155            relationships,
156        })
157    }
158
159    /// Extract relationships for an entity
160    async fn extract_relationships(
161        &self,
162        store: &RdfStore,
163        entity: &NamedNode,
164    ) -> PhysicsResult<Vec<EntityRelationship>> {
165        let query = format!(
166            r#"
167            PREFIX phys: <{prefix}>
168
169            SELECT ?predicate ?target WHERE {{
170                <{entity}> ?predicate ?target .
171            }}
172            "#,
173            prefix = self.config.physics_prefix,
174            entity = entity.as_str()
175        );
176
177        let results = store
178            .query(&query)
179            .map_err(|e| PhysicsError::RdfQuery(format!("Relationship query failed: {}", e)))?;
180
181        let mut relationships = Vec::new();
182
183        if let QueryResults::Bindings(ref bindings) = results.results() {
184            for binding in bindings {
185                if let (Some(Term::NamedNode(pred)), Some(Term::NamedNode(target))) =
186                    (binding.get("predicate"), binding.get("target"))
187                {
188                    relationships.push(EntityRelationship {
189                        predicate: pred.clone(),
190                        target: target.clone(),
191                        relationship_type: self.infer_relationship_type(pred.as_str()),
192                    });
193                }
194            }
195        }
196
197        Ok(relationships)
198    }
199
200    /// Infer relationship type from predicate
201    fn infer_relationship_type(&self, predicate: &str) -> RelationType {
202        let pred_lower = predicate.to_lowercase();
203        if pred_lower.contains("connect") {
204            RelationType::Connection
205        } else if pred_lower.contains("constrain") {
206            RelationType::Constraint
207        } else if pred_lower.contains("force") || pred_lower.contains("load") {
208            RelationType::Force
209        } else {
210            RelationType::Other
211        }
212    }
213
214    /// Parse a term into a physical value
215    fn parse_value(&self, term: &Term, unit: &str) -> PhysicsResult<Option<PhysicalValue>> {
216        match term {
217            Term::Literal(lit) => {
218                let value_str = lit.value();
219
220                // Try to parse as f64
221                if let Ok(value) = value_str.parse::<f64>() {
222                    if self.config.validate {
223                        check_finite(value, "value").map_err(|e| {
224                            PhysicsError::ParameterExtraction(format!(
225                                "Invalid value (not finite): {}",
226                                e
227                            ))
228                        })?;
229                    }
230
231                    let datatype = NamedNode::new("http://www.w3.org/2001/XMLSchema#double")
232                        .map_err(|e| {
233                            PhysicsError::ParameterExtraction(format!(
234                                "Invalid datatype IRI: {}",
235                                e
236                            ))
237                        })?;
238
239                    Ok(Some(PhysicalValue {
240                        value,
241                        unit: unit.to_string(),
242                        datatype,
243                    }))
244                } else {
245                    Ok(None)
246                }
247            }
248            _ => Ok(None),
249        }
250    }
251
252    /// Query a single property
253    pub async fn query_property(
254        &self,
255        store: &RdfStore,
256        entity: &NamedNode,
257        property: &str,
258    ) -> PhysicsResult<Option<PhysicalValue>> {
259        let query = format!(
260            r#"
261            PREFIX phys: <{prefix}>
262
263            SELECT ?value ?unit WHERE {{
264                <{entity}> phys:{property} ?valueNode .
265                ?valueNode phys:value ?value .
266                OPTIONAL {{ ?valueNode phys:unit ?unit }}
267            }}
268            "#,
269            prefix = self.config.physics_prefix,
270            entity = entity.as_str(),
271            property = property
272        );
273
274        let results = store
275            .query(&query)
276            .map_err(|e| PhysicsError::RdfQuery(format!("Property query failed: {}", e)))?;
277
278        if let QueryResults::Bindings(ref bindings) = results.results() {
279            for binding in bindings {
280                if let Some(value_term) = binding.get("value") {
281                    let unit = binding
282                        .get("unit")
283                        .and_then(|t| {
284                            if let Term::Literal(lit) = t {
285                                Some(lit.value().to_string())
286                            } else {
287                                None
288                            }
289                        })
290                        .unwrap_or_else(|| "dimensionless".to_string());
291
292                    return self.parse_value(value_term, &unit);
293                }
294            }
295        }
296
297        Ok(None)
298    }
299
300    /// Convert unit
301    pub fn convert_unit(&self, value: f64, from: &str, to: &str) -> PhysicsResult<f64> {
302        self.unit_registry
303            .convert(value, from, to)
304            .map_err(|e| PhysicsError::UnitConversion(format!("Conversion failed: {}", e)))
305    }
306
307    /// Get fallback default value
308    pub fn fallback_to_default(&self, property: &str) -> PhysicalValue {
309        match property {
310            "mass" => PhysicalValue {
311                value: 1.0,
312                unit: "kg".to_string(),
313                datatype: NamedNode::new("http://www.w3.org/2001/XMLSchema#double")
314                    .ok()
315                    .unwrap_or_else(|| panic!("Invalid datatype IRI")),
316            },
317            "temperature" => PhysicalValue {
318                value: 293.15,
319                unit: "K".to_string(),
320                datatype: NamedNode::new("http://www.w3.org/2001/XMLSchema#double")
321                    .ok()
322                    .unwrap_or_else(|| panic!("Invalid datatype IRI")),
323            },
324            _ => PhysicalValue {
325                value: 0.0,
326                unit: "dimensionless".to_string(),
327                datatype: NamedNode::new("http://www.w3.org/2001/XMLSchema#double")
328                    .ok()
329                    .unwrap_or_else(|| panic!("Invalid datatype IRI")),
330            },
331        }
332    }
333
334    /// Extract from RDF store
335    async fn extract_from_rdf(
336        &self,
337        store: &RdfStore,
338        entity_iri: &str,
339        simulation_type: &str,
340    ) -> PhysicsResult<SimulationParameters> {
341        let entity = self.extract_entity(store, entity_iri).await?;
342
343        // Convert physical entity to simulation parameters
344        let mut initial_conditions = HashMap::new();
345        let mut material_properties = HashMap::new();
346
347        for (key, physical_value) in entity.properties.iter() {
348            // Determine if this is an initial condition or material property
349            if key.contains("initial") || key.contains("position") || key.contains("velocity") {
350                initial_conditions.insert(
351                    key.clone(),
352                    PhysicalQuantity {
353                        value: physical_value.value,
354                        unit: physical_value.unit.clone(),
355                        uncertainty: None,
356                    },
357                );
358            } else if key.contains("modulus")
359                || key.contains("density")
360                || key.contains("conductivity")
361                || key.contains("capacity")
362            {
363                material_properties.insert(
364                    key.clone(),
365                    MaterialProperty {
366                        name: key.clone(),
367                        value: physical_value.value,
368                        unit: physical_value.unit.clone(),
369                    },
370                );
371            }
372        }
373
374        // Add defaults if needed
375        if self.config.use_defaults && initial_conditions.is_empty() {
376            match simulation_type {
377                "thermal" => {
378                    initial_conditions.insert(
379                        "temperature".to_string(),
380                        PhysicalQuantity {
381                            value: 293.15,
382                            unit: "K".to_string(),
383                            uncertainty: Some(0.1),
384                        },
385                    );
386                }
387                "mechanical" => {
388                    initial_conditions.insert(
389                        "displacement".to_string(),
390                        PhysicalQuantity {
391                            value: 0.0,
392                            unit: "m".to_string(),
393                            uncertainty: Some(1e-6),
394                        },
395                    );
396                }
397                _ => {}
398            }
399        }
400
401        Ok(SimulationParameters {
402            entity_iri: entity_iri.to_string(),
403            simulation_type: simulation_type.to_string(),
404            initial_conditions,
405            boundary_conditions: Vec::new(),
406            time_span: (0.0, 100.0),
407            time_steps: 100,
408            material_properties,
409            constraints: Vec::new(),
410        })
411    }
412
413    /// Mock parameter extraction for testing and development
414    async fn extract_mock_parameters(
415        &self,
416        entity_iri: &str,
417        simulation_type: &str,
418    ) -> PhysicsResult<SimulationParameters> {
419        // Generate reasonable default parameters based on simulation type
420        let (initial_conditions, material_properties) = match simulation_type {
421            "thermal" => {
422                let mut ic = HashMap::new();
423                ic.insert(
424                    "temperature".to_string(),
425                    PhysicalQuantity {
426                        value: 293.15, // 20°C in Kelvin
427                        unit: "K".to_string(),
428                        uncertainty: Some(0.1),
429                    },
430                );
431
432                let mut mp = HashMap::new();
433                mp.insert(
434                    "thermal_conductivity".to_string(),
435                    MaterialProperty {
436                        name: "Thermal Conductivity".to_string(),
437                        value: 1.0,
438                        unit: "W/(m*K)".to_string(),
439                    },
440                );
441                mp.insert(
442                    "specific_heat".to_string(),
443                    MaterialProperty {
444                        name: "Specific Heat Capacity".to_string(),
445                        value: 4186.0,
446                        unit: "J/(kg*K)".to_string(),
447                    },
448                );
449                mp.insert(
450                    "density".to_string(),
451                    MaterialProperty {
452                        name: "Density".to_string(),
453                        value: 1000.0,
454                        unit: "kg/m^3".to_string(),
455                    },
456                );
457
458                (ic, mp)
459            }
460            "mechanical" => {
461                let mut ic = HashMap::new();
462                ic.insert(
463                    "displacement".to_string(),
464                    PhysicalQuantity {
465                        value: 0.0,
466                        unit: "m".to_string(),
467                        uncertainty: Some(1e-6),
468                    },
469                );
470
471                let mut mp = HashMap::new();
472                mp.insert(
473                    "youngs_modulus".to_string(),
474                    MaterialProperty {
475                        name: "Young's Modulus".to_string(),
476                        value: 200e9, // Steel: 200 GPa
477                        unit: "Pa".to_string(),
478                    },
479                );
480                mp.insert(
481                    "poisson_ratio".to_string(),
482                    MaterialProperty {
483                        name: "Poisson's Ratio".to_string(),
484                        value: 0.3,
485                        unit: "dimensionless".to_string(),
486                    },
487                );
488
489                (ic, mp)
490            }
491            _ => (HashMap::new(), HashMap::new()),
492        };
493
494        Ok(SimulationParameters {
495            entity_iri: entity_iri.to_string(),
496            simulation_type: simulation_type.to_string(),
497            initial_conditions,
498            boundary_conditions: Vec::new(),
499            time_span: (0.0, 100.0),
500            time_steps: 100,
501            material_properties,
502            constraints: Vec::new(),
503        })
504    }
505}
506
507impl Default for ParameterExtractor {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513/// Physical entity extracted from RDF
514#[derive(Debug, Clone)]
515pub struct PhysicalEntity {
516    pub uri: NamedNode,
517    pub properties: HashMap<String, PhysicalValue>,
518    pub relationships: Vec<EntityRelationship>,
519}
520
521/// Physical value with unit
522#[derive(Debug, Clone)]
523pub struct PhysicalValue {
524    pub value: f64,
525    pub unit: String,
526    pub datatype: NamedNode,
527}
528
529/// Entity relationship
530#[derive(Debug, Clone)]
531pub struct EntityRelationship {
532    pub predicate: NamedNode,
533    pub target: NamedNode,
534    pub relationship_type: RelationType,
535}
536
537/// Relationship type
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum RelationType {
540    Connection,
541    Constraint,
542    Force,
543    Other,
544}
545
546/// Simulation Parameters
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct SimulationParameters {
549    /// Entity IRI being simulated
550    pub entity_iri: String,
551
552    /// Simulation type (e.g., "thermal", "fluid", "structural")
553    pub simulation_type: String,
554
555    /// Initial conditions
556    pub initial_conditions: HashMap<String, PhysicalQuantity>,
557
558    /// Boundary conditions
559    pub boundary_conditions: Vec<BoundaryCondition>,
560
561    /// Time span (start, end)
562    pub time_span: (f64, f64),
563
564    /// Number of time steps
565    pub time_steps: usize,
566
567    /// Material properties
568    pub material_properties: HashMap<String, MaterialProperty>,
569
570    /// Physics constraints
571    pub constraints: Vec<String>,
572}
573
574/// Physical quantity with value and unit
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct PhysicalQuantity {
577    pub value: f64,
578    pub unit: String,
579    pub uncertainty: Option<f64>,
580}
581
582/// Boundary condition
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct BoundaryCondition {
585    pub boundary_name: String,
586    pub condition_type: String,
587    pub value: PhysicalQuantity,
588}
589
590/// Material property
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct MaterialProperty {
593    pub name: String,
594    pub value: f64,
595    pub unit: String,
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[tokio::test]
603    async fn test_parameter_extractor_thermal() {
604        let extractor = ParameterExtractor::new();
605
606        let params = extractor
607            .extract("urn:example:battery:001", "thermal")
608            .await
609            .expect("Failed to extract parameters");
610
611        assert_eq!(params.entity_iri, "urn:example:battery:001");
612        assert_eq!(params.simulation_type, "thermal");
613        assert_eq!(params.time_steps, 100);
614        assert_eq!(params.time_span, (0.0, 100.0));
615
616        // Check thermal initial conditions
617        let temp = params
618            .initial_conditions
619            .get("temperature")
620            .expect("Missing temperature");
621        assert_eq!(temp.value, 293.15); // 20°C
622        assert_eq!(temp.unit, "K");
623
624        // Check material properties
625        assert!(params
626            .material_properties
627            .contains_key("thermal_conductivity"));
628        assert!(params.material_properties.contains_key("specific_heat"));
629        assert!(params.material_properties.contains_key("density"));
630    }
631
632    #[tokio::test]
633    async fn test_parameter_extractor_mechanical() {
634        let extractor = ParameterExtractor::new();
635
636        let params = extractor
637            .extract("urn:example:beam:001", "mechanical")
638            .await
639            .expect("Failed to extract parameters");
640
641        assert_eq!(params.simulation_type, "mechanical");
642
643        // Check mechanical initial conditions
644        let disp = params
645            .initial_conditions
646            .get("displacement")
647            .expect("Missing displacement");
648        assert_eq!(disp.value, 0.0);
649        assert_eq!(disp.unit, "m");
650
651        // Check mechanical properties
652        let youngs = params
653            .material_properties
654            .get("youngs_modulus")
655            .expect("Missing Young's modulus");
656        assert_eq!(youngs.value, 200e9); // Steel
657        assert_eq!(youngs.unit, "Pa");
658
659        let poisson = params
660            .material_properties
661            .get("poisson_ratio")
662            .expect("Missing Poisson's ratio");
663        assert_eq!(poisson.value, 0.3);
664        assert_eq!(poisson.unit, "dimensionless");
665    }
666
667    #[tokio::test]
668    async fn test_unit_conversion() {
669        let extractor = ParameterExtractor::new();
670
671        // g → kg
672        let kg_value = extractor
673            .convert_unit(1000.0, "g", "kg")
674            .expect("Failed to convert g to kg");
675        assert!((kg_value - 1.0).abs() < 1e-10);
676
677        // cm → m
678        let m_value = extractor
679            .convert_unit(100.0, "cm", "m")
680            .expect("Failed to convert cm to m");
681        assert!((m_value - 1.0).abs() < 1e-10);
682    }
683
684    #[tokio::test]
685    async fn test_fallback_defaults() {
686        let extractor = ParameterExtractor::new();
687
688        let mass_default = extractor.fallback_to_default("mass");
689        assert_eq!(mass_default.value, 1.0);
690        assert_eq!(mass_default.unit, "kg");
691
692        let temp_default = extractor.fallback_to_default("temperature");
693        assert_eq!(temp_default.value, 293.15);
694        assert_eq!(temp_default.unit, "K");
695    }
696
697    #[test]
698    fn test_physical_quantity_serialization() {
699        let quantity = PhysicalQuantity {
700            value: 300.0,
701            unit: "K".to_string(),
702            uncertainty: Some(0.5),
703        };
704
705        let json = serde_json::to_string(&quantity).expect("Failed to serialize");
706        let deserialized: PhysicalQuantity =
707            serde_json::from_str(&json).expect("Failed to deserialize");
708
709        assert_eq!(deserialized.value, 300.0);
710        assert_eq!(deserialized.unit, "K");
711        assert_eq!(deserialized.uncertainty, Some(0.5));
712    }
713
714    #[test]
715    fn test_simulation_parameters_serialization() {
716        let mut ic = HashMap::new();
717        ic.insert(
718            "temperature".to_string(),
719            PhysicalQuantity {
720                value: 293.15,
721                unit: "K".to_string(),
722                uncertainty: None,
723            },
724        );
725
726        let params = SimulationParameters {
727            entity_iri: "urn:test".to_string(),
728            simulation_type: "thermal".to_string(),
729            initial_conditions: ic,
730            boundary_conditions: Vec::new(),
731            time_span: (0.0, 100.0),
732            time_steps: 50,
733            material_properties: HashMap::new(),
734            constraints: Vec::new(),
735        };
736
737        let json = serde_json::to_string(&params).expect("Failed to serialize");
738        let deserialized: SimulationParameters =
739            serde_json::from_str(&json).expect("Failed to deserialize");
740
741        assert_eq!(deserialized.entity_iri, "urn:test");
742        assert_eq!(deserialized.simulation_type, "thermal");
743        assert_eq!(deserialized.time_steps, 50);
744    }
745
746    #[tokio::test]
747    async fn test_parameter_extractor_unknown_type() {
748        let extractor = ParameterExtractor::new();
749
750        let params = extractor
751            .extract("urn:example:entity", "unknown_type")
752            .await
753            .expect("Failed to extract parameters");
754
755        // Should return empty parameters for unknown types
756        assert!(params.initial_conditions.is_empty());
757        assert!(params.material_properties.is_empty());
758    }
759
760    #[test]
761    fn test_extraction_config() {
762        let config = ExtractionConfig {
763            use_defaults: false,
764            physics_prefix: "http://example.org/phys#".to_string(),
765            validate: true,
766        };
767
768        assert!(!config.use_defaults);
769        assert_eq!(config.physics_prefix, "http://example.org/phys#");
770        assert!(config.validate);
771    }
772
773    #[test]
774    fn test_relationship_type_inference() {
775        let extractor = ParameterExtractor::new();
776
777        assert_eq!(
778            extractor.infer_relationship_type("http://example.org/connects"),
779            RelationType::Connection
780        );
781        assert_eq!(
782            extractor.infer_relationship_type("http://example.org/constrains"),
783            RelationType::Constraint
784        );
785        assert_eq!(
786            extractor.infer_relationship_type("http://example.org/applies_force"),
787            RelationType::Force
788        );
789        assert_eq!(
790            extractor.infer_relationship_type("http://example.org/other_relation"),
791            RelationType::Other
792        );
793    }
794}