1use 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
15pub struct ParameterExtractor {
17 store: Option<Arc<RdfStore>>,
19 unit_registry: UnitRegistry,
21 config: ExtractionConfig,
23}
24
25#[derive(Debug, Clone)]
27pub struct ExtractionConfig {
28 pub use_defaults: bool,
30 pub physics_prefix: String,
32 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 pub fn new() -> Self {
49 Self {
50 store: None,
51 unit_registry: UnitRegistry::new(),
52 config: ExtractionConfig::default(),
53 }
54 }
55
56 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 pub fn with_config(mut self, config: ExtractionConfig) -> Self {
67 self.config = config;
68 self
69 }
70
71 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 self.extract_mock_parameters(entity_iri, simulation_type)
83 .await
84 }
85 }
86
87 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 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 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 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 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 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 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 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 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 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 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 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 let mut initial_conditions = HashMap::new();
345 let mut material_properties = HashMap::new();
346
347 for (key, physical_value) in entity.properties.iter() {
348 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 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 async fn extract_mock_parameters(
415 &self,
416 entity_iri: &str,
417 simulation_type: &str,
418 ) -> PhysicsResult<SimulationParameters> {
419 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, 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, 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#[derive(Debug, Clone)]
515pub struct PhysicalEntity {
516 pub uri: NamedNode,
517 pub properties: HashMap<String, PhysicalValue>,
518 pub relationships: Vec<EntityRelationship>,
519}
520
521#[derive(Debug, Clone)]
523pub struct PhysicalValue {
524 pub value: f64,
525 pub unit: String,
526 pub datatype: NamedNode,
527}
528
529#[derive(Debug, Clone)]
531pub struct EntityRelationship {
532 pub predicate: NamedNode,
533 pub target: NamedNode,
534 pub relationship_type: RelationType,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum RelationType {
540 Connection,
541 Constraint,
542 Force,
543 Other,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct SimulationParameters {
549 pub entity_iri: String,
551
552 pub simulation_type: String,
554
555 pub initial_conditions: HashMap<String, PhysicalQuantity>,
557
558 pub boundary_conditions: Vec<BoundaryCondition>,
560
561 pub time_span: (f64, f64),
563
564 pub time_steps: usize,
566
567 pub material_properties: HashMap<String, MaterialProperty>,
569
570 pub constraints: Vec<String>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct PhysicalQuantity {
577 pub value: f64,
578 pub unit: String,
579 pub uncertainty: Option<f64>,
580}
581
582#[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#[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 let temp = params
618 .initial_conditions
619 .get("temperature")
620 .expect("Missing temperature");
621 assert_eq!(temp.value, 293.15); assert_eq!(temp.unit, "K");
623
624 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 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 let youngs = params
653 .material_properties
654 .get("youngs_modulus")
655 .expect("Missing Young's modulus");
656 assert_eq!(youngs.value, 200e9); 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 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 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(¶ms).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 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}