1use crate::coupling::Coupling;
4use crate::domain::Domain;
5use crate::error::{Result, TauFormatError};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PhyzSpec {
12 pub version: String,
14 pub name: String,
16 #[serde(default)]
18 pub description: String,
19 pub world: WorldConfig,
21 pub domains: HashMap<String, Domain>,
23 #[serde(default)]
25 pub couplings: Vec<Coupling>,
26 #[serde(default)]
28 pub parameters: HashMap<String, ParameterSpec>,
29 #[serde(default)]
31 pub importers: Vec<ImporterSpec>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct WorldConfig {
37 pub gravity: [f64; 3],
39 pub dt: f64,
41 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ContactMaterial {
59 #[serde(default = "default_stiffness")]
61 pub stiffness: f64,
62 #[serde(default = "default_damping")]
64 pub damping: f64,
65 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ParameterSpec {
95 #[serde(rename = "type")]
97 pub param_type: ParameterType,
98 pub value: serde_json::Value,
100 #[serde(default)]
102 pub uncertainty: Option<f64>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "lowercase")]
108pub enum ParameterType {
109 Scalar,
111 Vector,
113 Distribution,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ImporterSpec {
120 pub format: ImportFormat,
122 pub source: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128#[serde(rename_all = "lowercase")]
129pub enum ImportFormat {
130 Mjcf,
132 Urdf,
134 Usd,
136 Sdf,
138}
139
140pub 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 if spec.version.is_empty() {
147 return Err(TauFormatError::MissingField("version".to_string()));
148 }
149
150 Ok(spec)
151}
152
153pub 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
160pub 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(¶m).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}