Skip to main content

phyz_format/
schema.rs

1//! Main .phyz format schema and loader.
2
3use crate::coupling::Coupling;
4use crate::domain::Domain;
5use crate::error::{Result, TauFormatError};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Top-level .phyz format specification.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PhyzSpec {
12    /// Format version.
13    pub version: String,
14    /// Model name.
15    pub name: String,
16    /// Optional description.
17    #[serde(default)]
18    pub description: String,
19    /// World configuration.
20    pub world: WorldConfig,
21    /// Physics domains.
22    pub domains: HashMap<String, Domain>,
23    /// Domain couplings.
24    #[serde(default)]
25    pub couplings: Vec<Coupling>,
26    /// Parameter specifications.
27    #[serde(default)]
28    pub parameters: HashMap<String, ParameterSpec>,
29    /// Import sources.
30    #[serde(default)]
31    pub importers: Vec<ImporterSpec>,
32}
33
34/// World-level configuration.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct WorldConfig {
37    /// Gravity vector [x, y, z].
38    pub gravity: [f64; 3],
39    /// Default time step.
40    pub dt: f64,
41    /// Default contact material properties.
42    #[serde(default)]
43    pub default_contact_material: ContactMaterial,
44}
45
46impl Default for WorldConfig {
47    fn default() -> Self {
48        Self {
49            gravity: [0.0, 0.0, -9.81],
50            dt: 0.001,
51            default_contact_material: ContactMaterial::default(),
52        }
53    }
54}
55
56/// Contact material properties.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ContactMaterial {
59    /// Contact stiffness.
60    #[serde(default = "default_stiffness")]
61    pub stiffness: f64,
62    /// Contact damping.
63    #[serde(default = "default_damping")]
64    pub damping: f64,
65    /// Friction coefficient.
66    #[serde(default = "default_friction")]
67    pub friction: f64,
68}
69
70fn default_stiffness() -> f64 {
71    10000.0
72}
73
74fn default_damping() -> f64 {
75    100.0
76}
77
78fn default_friction() -> f64 {
79    0.5
80}
81
82impl Default for ContactMaterial {
83    fn default() -> Self {
84        Self {
85            stiffness: default_stiffness(),
86            damping: default_damping(),
87            friction: default_friction(),
88        }
89    }
90}
91
92/// Parameter specification with optional uncertainty.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ParameterSpec {
95    /// Parameter type.
96    #[serde(rename = "type")]
97    pub param_type: ParameterType,
98    /// Base value(s).
99    pub value: serde_json::Value,
100    /// Uncertainty (for probabilistic parameters).
101    #[serde(default)]
102    pub uncertainty: Option<f64>,
103}
104
105/// Parameter type.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "lowercase")]
108pub enum ParameterType {
109    /// Scalar parameter.
110    Scalar,
111    /// Vector parameter.
112    Vector,
113    /// Distribution parameter.
114    Distribution,
115}
116
117/// Import source specification.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ImporterSpec {
120    /// Source format.
121    pub format: ImportFormat,
122    /// Source file path.
123    pub source: String,
124}
125
126/// Supported import formats.
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128#[serde(rename_all = "lowercase")]
129pub enum ImportFormat {
130    /// MuJoCo MJCF XML.
131    Mjcf,
132    /// URDF XML.
133    Urdf,
134    /// USD (Universal Scene Description).
135    Usd,
136    /// SDF (Simulation Description Format).
137    Sdf,
138}
139
140/// Load a .phyz model from file.
141pub fn load_phyz_model(path: &str) -> Result<PhyzSpec> {
142    let json = std::fs::read_to_string(path)?;
143    let spec: PhyzSpec = serde_json::from_str(&json)?;
144
145    // Validate version
146    if spec.version.is_empty() {
147        return Err(TauFormatError::MissingField("version".to_string()));
148    }
149
150    Ok(spec)
151}
152
153/// Save a .phyz model to file.
154pub fn save_phyz_model(path: &str, spec: &PhyzSpec) -> Result<()> {
155    let json = serde_json::to_string_pretty(spec)?;
156    std::fs::write(path, json)?;
157    Ok(())
158}
159
160/// Export a PhyzSpec to JSON string.
161pub fn export_phyz(spec: &PhyzSpec) -> Result<String> {
162    Ok(serde_json::to_string_pretty(spec)?)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::domain::DomainType;
169
170    #[test]
171    fn test_world_config_serialization() {
172        let config = WorldConfig {
173            gravity: [0.0, 0.0, -9.81],
174            dt: 0.001,
175            default_contact_material: ContactMaterial::default(),
176        };
177
178        let json = serde_json::to_string(&config).unwrap();
179        let parsed: WorldConfig = serde_json::from_str(&json).unwrap();
180
181        assert_eq!(parsed.gravity, [0.0, 0.0, -9.81]);
182        assert_eq!(parsed.dt, 0.001);
183    }
184
185    #[test]
186    fn test_phyz_spec_serialization() {
187        let mut domains = HashMap::new();
188        let mut rigid_config = HashMap::new();
189        rigid_config.insert(
190            "bodies".to_string(),
191            serde_json::json!([{
192                "name": "link",
193                "mass": 1.0,
194                "inertia": [1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
195                "center_of_mass": [0.0, 0.0, 0.0]
196            }]),
197        );
198        rigid_config.insert("joints".to_string(), serde_json::json!([]));
199
200        domains.insert(
201            "rigid_body".to_string(),
202            Domain {
203                domain_type: DomainType::RigidBodyDynamics,
204                config: rigid_config,
205            },
206        );
207
208        let spec = PhyzSpec {
209            version: "1.0".to_string(),
210            name: "test-model".to_string(),
211            description: "Test model".to_string(),
212            world: WorldConfig::default(),
213            domains,
214            couplings: vec![],
215            parameters: HashMap::new(),
216            importers: vec![],
217        };
218
219        let json = serde_json::to_string_pretty(&spec).unwrap();
220        let parsed: PhyzSpec = serde_json::from_str(&json).unwrap();
221
222        assert_eq!(parsed.name, "test-model");
223        assert_eq!(parsed.version, "1.0");
224    }
225
226    #[test]
227    fn test_parameter_spec_serialization() {
228        let param = ParameterSpec {
229            param_type: ParameterType::Scalar,
230            value: serde_json::json!(1.0),
231            uncertainty: Some(0.1),
232        };
233
234        let json = serde_json::to_string(&param).unwrap();
235        let parsed: ParameterSpec = serde_json::from_str(&json).unwrap();
236
237        assert_eq!(parsed.param_type, ParameterType::Scalar);
238        assert_eq!(parsed.uncertainty, Some(0.1));
239    }
240
241    #[test]
242    fn test_importer_spec_serialization() {
243        let importer = ImporterSpec {
244            format: ImportFormat::Mjcf,
245            source: "model.xml".to_string(),
246        };
247
248        let json = serde_json::to_string(&importer).unwrap();
249        let parsed: ImporterSpec = serde_json::from_str(&json).unwrap();
250
251        assert_eq!(parsed.format, ImportFormat::Mjcf);
252        assert_eq!(parsed.source, "model.xml");
253    }
254}