use crate::error::{PhysicsError, PhysicsResult};
use crate::rdf::physics_rdf_types::{
PhysicsToRdfConfig, RdfBoundaryCondition, RdfMaterialProperty, Triple, NS_EX, NS_PHYS, NS_PROV,
NS_QUDT, NS_RDF, NS_RDFS, NS_SOSA, NS_SSN, NS_UNIT, NS_XSD,
};
use crate::simulation::result_injection::SimulationResult;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
pub(crate) fn qudt_unit_for(property: &str) -> &'static str {
match property {
"temperature" => "DEG_C",
"temperature_k" => "K",
"pressure" => "PA",
"velocity" | "velocity_x" | "velocity_y" | "velocity_z" => "M-PER-SEC",
"mass" => "KiloGM",
"energy" | "kinetic_energy" | "potential_energy" | "total_energy" => "J",
"power" => "W",
"force" | "force_x" | "force_y" | "force_z" => "N",
"density" => "KiloGM-PER-M3",
"viscosity" => "PA-SEC",
"thermal_conductivity" => "W-PER-M-K",
"specific_heat" => "J-PER-KiloGM-K",
"length" | "position_x" | "position_y" | "position_z" => "M",
"time" => "SEC",
"frequency" => "HZ",
"voltage" => "V",
"current" => "A",
"resistance" => "OHM",
"entropy" => "J-PER-K",
_ => "UNITLESS",
}
}
pub(crate) fn turtle_preamble() -> String {
use crate::rdf::physics_rdf_types::{NS_RDF as RDF, NS_RDFS as RDFS};
format!(
"@prefix rdf: <{RDF}> .\n\
@prefix rdfs: <{RDFS}> .\n\
@prefix xsd: <{NS_XSD}> .\n\
@prefix sosa: <{NS_SOSA}> .\n\
@prefix ssn: <{NS_SSN}> .\n\
@prefix qudt: <{NS_QUDT}> .\n\
@prefix unit: <{NS_UNIT}> .\n\
@prefix ex: <{NS_EX}> .\n\
@prefix phys: <{NS_PHYS}> .\n\
@prefix prov: <{NS_PROV}> .\n\n"
)
}
pub struct PhysicsToRdf {
pub base_iri: String,
pub include_provenance: bool,
pub include_digital_twin: bool,
pub include_units: bool,
}
impl Default for PhysicsToRdf {
fn default() -> Self {
Self {
base_iri: NS_EX.to_string(),
include_provenance: true,
include_digital_twin: true,
include_units: true,
}
}
}
impl PhysicsToRdf {
pub fn new() -> Self {
Self::default()
}
pub fn from_config(cfg: PhysicsToRdfConfig) -> Self {
Self {
base_iri: cfg.base_iri,
include_provenance: cfg.include_provenance,
include_digital_twin: cfg.include_digital_twin,
include_units: cfg.include_units,
}
}
pub fn convert(&self, result: &SimulationResult) -> Vec<Triple> {
let mut triples = Vec::new();
let run_id = sanitize_iri_fragment(&result.simulation_run_id);
let entity_frag = sanitize_iri_fragment(&result.entity_iri);
let ts_lit = format!(
"\"{}\"^^<{}dateTime>",
result.timestamp.format("%Y-%m-%dT%H:%M:%SZ"),
NS_XSD
);
if self.include_digital_twin {
let dt_iri = format!("<{}dt_{}>", self.base_iri, entity_frag);
triples.push(Triple::new(
dt_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}DigitalTwin>", NS_EX),
));
triples.push(Triple::new(
dt_iri.clone(),
format!("<{}label>", NS_RDFS),
format!("\"Digital Twin of {}\"", result.entity_iri),
));
}
for (t_idx, sv) in result.state_trajectory.iter().enumerate() {
let state_iri = format!("<{}state_{}_t{}>", self.base_iri, run_id, t_idx);
if self.include_digital_twin {
let dt_iri = format!("<{}dt_{}>", self.base_iri, entity_frag);
triples.push(Triple::new(
dt_iri,
format!("<{}hasState>", NS_EX),
state_iri.clone(),
));
triples.push(Triple::new(
state_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}SimulationState>", NS_EX),
));
triples.push(Triple::new(
state_iri.clone(),
format!("<{}simulationRunId>", NS_EX),
format!("\"{}\"^^<{}string>", result.simulation_run_id, NS_XSD),
));
triples.push(Triple::new(
state_iri.clone(),
format!("<{}timestamp>", NS_EX),
ts_lit.clone(),
));
triples.push(Triple::new(
state_iri.clone(),
format!("<{}simTime>", NS_PHYS),
format!("\"{}\"^^<{}double>", sv.time, NS_XSD),
));
}
for (prop, &val) in &sv.state {
let prop_frag = sanitize_iri_fragment(prop);
let obs_iri = format!("<{}obs_{}_{}_{}>", self.base_iri, run_id, prop_frag, t_idx);
let prop_iri = format!("<{}{}>", NS_PHYS, prop_frag);
let feature_iri = format!("<{}dt_{}>", self.base_iri, entity_frag);
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}Observation>", NS_SOSA),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}observedProperty>", NS_SOSA),
prop_iri.clone(),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}hasSimpleResult>", NS_SOSA),
format!("\"{}\"^^<{}double>", val, NS_XSD),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}resultTime>", NS_SOSA),
ts_lit.clone(),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}hasFeatureOfInterest>", NS_SOSA),
feature_iri,
));
triples.push(Triple::new(
prop_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}ObservableProperty>", NS_SOSA),
));
triples.push(Triple::new(
prop_iri.clone(),
format!("<{}label>", NS_RDFS),
format!("\"{}\"", prop),
));
if self.include_units {
let qudt_unit = qudt_unit_for(prop);
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}unit>", NS_QUDT),
format!("<{}{}>", NS_UNIT, qudt_unit),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}numericValue>", NS_QUDT),
format!("\"{}\"^^<{}double>", val, NS_XSD),
));
}
if self.include_digital_twin {
triples.push(Triple::new(
state_iri.clone(),
format!("<{}hasObservation>", NS_SSN),
obs_iri,
));
}
}
}
for (prop, &val) in &result.derived_quantities {
let prop_frag = sanitize_iri_fragment(prop);
let obs_iri = format!("<{}derived_{}_{}>", self.base_iri, run_id, prop_frag);
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}Observation>", NS_SOSA),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}observedProperty>", NS_SOSA),
format!("<{}{}>", NS_PHYS, prop_frag),
));
triples.push(Triple::new(
obs_iri.clone(),
format!("<{}hasSimpleResult>", NS_SOSA),
format!("\"{}\"^^<{}double>", val, NS_XSD),
));
}
if self.include_provenance {
let activity_iri = format!("<{}activity_{}>", self.base_iri, run_id);
triples.push(Triple::new(
activity_iri.clone(),
format!("<{}type>", NS_RDF),
format!("<{}Activity>", NS_PROV),
));
triples.push(Triple::new(
activity_iri.clone(),
format!("<{}startedAtTime>", NS_PROV),
ts_lit.clone(),
));
triples.push(Triple::new(
activity_iri.clone(),
format!("<{}wasAssociatedWith>", NS_PROV),
format!(
"<{}software/{}>",
NS_PHYS,
sanitize_iri_fragment(&result.provenance.software)
),
));
triples.push(Triple::new(
activity_iri.clone(),
format!("<{}converged>", NS_PHYS),
format!(
"\"{}\"^^<{}boolean>",
result.convergence_info.converged, NS_XSD
),
));
}
triples
}
pub fn to_turtle(&self, result: &SimulationResult) -> String {
let triples = self.convert(result);
let mut out = turtle_preamble();
for t in &triples {
let _ = writeln!(out, "{} .\n", t.to_turtle_statement());
}
out
}
pub fn to_subject_map(&self, result: &SimulationResult) -> HashMap<String, Vec<Triple>> {
let mut map: HashMap<String, Vec<Triple>> = HashMap::new();
for t in self.convert(result) {
map.entry(t.subject.clone()).or_default().push(t);
}
map
}
}
pub struct RdfToPhysics {
pub phys_ns: String,
pub lenient: bool,
}
impl Default for RdfToPhysics {
fn default() -> Self {
Self {
phys_ns: NS_PHYS.to_string(),
lenient: true,
}
}
}
impl RdfToPhysics {
pub fn new() -> Self {
Self::default()
}
pub fn extract_boundary_conditions(
&self,
triples: &[Triple],
) -> PhysicsResult<Vec<RdfBoundaryCondition>> {
let by_subject = group_by_subject(triples);
let mut bcs = Vec::new();
let bc_type_iri = format!("<{}BoundaryCondition>", NS_EX);
let bc_type_iri2 = format!("<{}BoundaryCondition>", self.phys_ns);
for (subj, props) in &by_subject {
let is_bc = props
.iter()
.any(|t| is_rdf_type(t) && (t.object == bc_type_iri || t.object == bc_type_iri2));
if !is_bc {
continue;
}
let condition_type = find_object_str(props, &format!("<{}bcType>", self.phys_ns))
.or_else(|| find_object_str(props, &format!("<{}conditionType>", self.phys_ns)))
.unwrap_or_else(|| "unspecified".to_string());
let property = find_object_str(props, &format!("<{}bcProperty>", self.phys_ns))
.or_else(|| find_object_str(props, &format!("<{}observedProperty>", NS_SOSA)))
.unwrap_or_else(|| "unknown".to_string());
let value =
find_object_double(props, &format!("<{}bcValue>", self.phys_ns)).unwrap_or(0.0);
let unit = find_object_str(props, &format!("<{}bcUnit>", self.phys_ns))
.unwrap_or_else(|| "UNITLESS".to_string());
bcs.push(RdfBoundaryCondition {
iri: subj.clone(),
condition_type,
property,
value,
unit,
});
}
if bcs.is_empty() && !self.lenient {
return Err(PhysicsError::ParameterExtraction(
"no boundary conditions found in triples".to_string(),
));
}
Ok(bcs)
}
pub fn extract_material_properties(
&self,
triples: &[Triple],
) -> PhysicsResult<Vec<RdfMaterialProperty>> {
let by_subject = group_by_subject(triples);
let mut mats = Vec::new();
let mat_type_iri = format!("<{}Material>", NS_EX);
let mat_type_iri2 = format!("<{}Material>", self.phys_ns);
for (subj, props) in &by_subject {
let is_mat = props
.iter()
.any(|t| is_rdf_type(t) && (t.object == mat_type_iri || t.object == mat_type_iri2));
if !is_mat {
continue;
}
let name =
find_object_str(props, &format!("<{}label>", NS_RDFS)).unwrap_or_else(|| {
let stripped = subj
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(subj.as_str());
stripped
.rsplit(['/', '#'])
.next()
.unwrap_or("unknown")
.to_string()
});
let value =
find_object_double(props, &format!("<{}value>", self.phys_ns)).unwrap_or(0.0);
let unit = find_object_str(props, &format!("<{}unit>", self.phys_ns))
.unwrap_or_else(|| "UNITLESS".to_string());
let description = find_object_str(props, &format!("<{}description>", NS_RDFS));
mats.push(RdfMaterialProperty {
iri: subj.clone(),
name,
value,
unit,
description,
});
}
Ok(mats)
}
pub fn extract_observations(&self, triples: &[Triple]) -> Vec<(String, f64)> {
let by_subject = group_by_subject(triples);
let obs_type = format!("<{}Observation>", NS_SOSA);
let rdf_type_pred = format!("<{}type>", NS_RDF);
let observed_prop_pred = format!("<{}observedProperty>", NS_SOSA);
let simple_result_pred = format!("<{}hasSimpleResult>", NS_SOSA);
let mut results = Vec::new();
for props in by_subject.values() {
let is_obs = props
.iter()
.any(|t| t.predicate == rdf_type_pred && t.object == obs_type);
if !is_obs {
continue;
}
let prop = find_object_str(props, &observed_prop_pred).unwrap_or_default();
let value = find_object_double(props, &simple_result_pred);
if let Some(v) = value {
results.push((prop, v));
}
}
results
}
}
pub(crate) fn sanitize_iri_fragment(s: &str) -> String {
s.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => c,
_ => '_',
})
.collect()
}
pub(crate) fn group_by_subject(triples: &[Triple]) -> HashMap<String, Vec<Triple>> {
let mut map: HashMap<String, Vec<Triple>> = HashMap::new();
for t in triples {
map.entry(t.subject.clone()).or_default().push(t.clone());
}
map
}
pub(crate) fn is_rdf_type(t: &Triple) -> bool {
t.predicate == format!("<{}type>", NS_RDF)
|| t.predicate == "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
|| t.predicate == "a"
}
pub(crate) fn find_object_str(props: &[Triple], predicate: &str) -> Option<String> {
props
.iter()
.find(|t| t.predicate == predicate)
.map(|t| strip_literal_quotes(&t.object))
}
pub(crate) fn find_object_double(props: &[Triple], predicate: &str) -> Option<f64> {
props
.iter()
.find(|t| t.predicate == predicate)
.and_then(|t| extract_double_literal(&t.object))
}
pub(crate) fn strip_literal_quotes(s: &str) -> String {
let trimmed = s.trim();
let without_dt = if let Some(pos) = trimmed.rfind("^^") {
&trimmed[..pos]
} else {
trimmed
};
without_dt.trim_matches('"').to_string()
}
pub(crate) fn extract_double_literal(s: &str) -> Option<f64> {
strip_literal_quotes(s).trim().parse::<f64>().ok()
}