Skip to main content

phyz_world/
generator.rs

1//! Procedural world generation for creating random articulated systems.
2
3use rand::prelude::*;
4use phyz_math::{GRAVITY, Mat3, SpatialInertia, SpatialTransform, Vec3};
5use phyz_model::{Model, ModelBuilder};
6
7/// Procedural world generator with seeded randomness.
8pub struct WorldGenerator {
9    #[allow(dead_code)]
10    seed: u64,
11    rng: StdRng,
12}
13
14impl WorldGenerator {
15    /// Create a new world generator with the given seed.
16    pub fn new(seed: u64) -> Self {
17        Self {
18            seed,
19            rng: StdRng::seed_from_u64(seed),
20        }
21    }
22
23    /// Generate a random kinematic chain with n links.
24    ///
25    /// Each link has randomized mass and length within the given ranges.
26    /// All joints are revolute joints about the Z axis.
27    pub fn random_chain(
28        &mut self,
29        nlinkages: usize,
30        mass_range: [f64; 2],
31        length_range: [f64; 2],
32    ) -> Model {
33        let mut builder = ModelBuilder::new()
34            .gravity(Vec3::new(0.0, 0.0, -GRAVITY))
35            .dt(0.001);
36
37        for i in 0..nlinkages {
38            let parent = if i == 0 { -1 } else { (i - 1) as i32 };
39            let mass = self.rng.gen_range(mass_range[0]..=mass_range[1]);
40            let length = self.rng.gen_range(length_range[0]..=length_range[1]);
41
42            let parent_to_joint = if i == 0 {
43                SpatialTransform::identity()
44            } else {
45                // Previous link's endpoint
46                SpatialTransform::translation(Vec3::new(0.0, 0.0, -length))
47            };
48
49            // Cylinder inertia: I_xx = I_yy = m*L^2/12 + m*(L/2)^2 = m*L^2/3
50            // I_zz = m*r^2/2 ≈ 0 for thin rod
51            let com = Vec3::new(0.0, 0.0, -length / 2.0);
52            let inertia_val = mass * length * length / 3.0;
53            let inertia = SpatialInertia::new(
54                mass,
55                com,
56                Mat3::from_diagonal(&Vec3::new(inertia_val, inertia_val, 0.0)),
57            );
58
59            builder =
60                builder.add_revolute_body(&format!("link{}", i), parent, parent_to_joint, inertia);
61        }
62
63        builder.build()
64    }
65
66    /// Generate a random quadruped robot.
67    ///
68    /// Creates a body with 4 legs, each leg having 2 revolute joints.
69    /// Returns a model with 1 body + 4*2 = 8 linkages (9 bodies total including torso).
70    pub fn random_quadruped(&mut self) -> Model {
71        let body_mass = 5.0;
72        let body_size = 0.4;
73        let leg_mass = 0.5;
74        let leg_length = 0.3;
75
76        let mut builder = ModelBuilder::new()
77            .gravity(Vec3::new(0.0, 0.0, -GRAVITY))
78            .dt(0.001);
79
80        // Torso (free-floating body)
81        let torso_inertia = SpatialInertia::new(
82            body_mass,
83            Vec3::zeros(),
84            Mat3::from_diagonal(&Vec3::new(
85                body_mass * body_size * body_size / 6.0,
86                body_mass * body_size * body_size / 6.0,
87                body_mass * body_size * body_size / 6.0,
88            )),
89        );
90        builder = builder.add_free_body("torso", -1, SpatialTransform::identity(), torso_inertia);
91
92        // Four legs: each leg has 2 segments
93        let leg_positions = [
94            Vec3::new(body_size / 2.0, body_size / 2.0, 0.0),
95            Vec3::new(body_size / 2.0, -body_size / 2.0, 0.0),
96            Vec3::new(-body_size / 2.0, body_size / 2.0, 0.0),
97            Vec3::new(-body_size / 2.0, -body_size / 2.0, 0.0),
98        ];
99
100        for (leg_idx, leg_pos) in leg_positions.iter().enumerate() {
101            // Upper leg segment attached to torso
102            let upper_leg_inertia = SpatialInertia::new(
103                leg_mass,
104                Vec3::new(0.0, 0.0, -leg_length / 2.0),
105                Mat3::from_diagonal(&Vec3::new(
106                    leg_mass * leg_length * leg_length / 12.0,
107                    leg_mass * leg_length * leg_length / 12.0,
108                    0.0,
109                )),
110            );
111            let upper_joint = SpatialTransform::translation(*leg_pos);
112            builder = builder.add_revolute_body(
113                &format!("leg{}_upper", leg_idx),
114                0, // parent is torso (body 0)
115                upper_joint,
116                upper_leg_inertia,
117            );
118
119            // Lower leg segment attached to upper leg
120            let lower_leg_inertia = upper_leg_inertia;
121            let lower_joint = SpatialTransform::translation(Vec3::new(0.0, 0.0, -leg_length));
122            let upper_body_idx = 1 + leg_idx * 2;
123            builder = builder.add_revolute_body(
124                &format!("leg{}_lower", leg_idx),
125                upper_body_idx as i32,
126                lower_joint,
127                lower_leg_inertia,
128            );
129        }
130
131        builder.build()
132    }
133
134    /// Generate a platform with randomly placed obstacles.
135    ///
136    /// Creates a flat platform (fixed body) with n obstacles of varying heights.
137    /// Obstacles are modeled as fixed bodies with box collision geometry.
138    pub fn platform_with_obstacles(
139        &mut self,
140        width: f64,
141        nobs: usize,
142        obs_height_range: [f64; 2],
143    ) -> Model {
144        let mut builder = ModelBuilder::new()
145            .gravity(Vec3::new(0.0, 0.0, -GRAVITY))
146            .dt(0.001);
147
148        // Platform (fixed body at origin)
149        let platform_inertia = SpatialInertia::new(
150            1000.0, // large mass for stability (won't move anyway)
151            Vec3::zeros(),
152            Mat3::identity(),
153        );
154        builder = builder.add_fixed_body(
155            "platform",
156            -1,
157            SpatialTransform::identity(),
158            platform_inertia,
159        );
160
161        // Random obstacles
162        for i in 0..nobs {
163            let x = self.rng.gen_range(-width / 2.0..width / 2.0);
164            let y = self.rng.gen_range(-width / 2.0..width / 2.0);
165            let height = self
166                .rng
167                .gen_range(obs_height_range[0]..=obs_height_range[1]);
168
169            let obs_pos = Vec3::new(x, y, height / 2.0);
170            let obs_inertia = SpatialInertia::new(
171                10.0,
172                Vec3::zeros(),
173                Mat3::from_diagonal(&Vec3::new(0.1, 0.1, 0.1)),
174            );
175
176            builder = builder.add_fixed_body(
177                &format!("obstacle{}", i),
178                0, // parent is platform
179                SpatialTransform::translation(obs_pos),
180                obs_inertia,
181            );
182        }
183
184        builder.build()
185    }
186
187    /// Generate a random tree-like structure.
188    ///
189    /// Creates a branching kinematic tree with random splits.
190    /// Useful for testing tree traversal algorithms.
191    pub fn random_tree(&mut self, depth: usize, branching_factor: usize) -> Model {
192        let mut builder = ModelBuilder::new()
193            .gravity(Vec3::new(0.0, 0.0, -GRAVITY))
194            .dt(0.001);
195
196        let mass = 1.0;
197        let length = 0.5;
198
199        // Build tree level by level
200        let mut body_count: i32 = 0;
201        let mut current_level = vec![(None, 0)]; // (parent_idx, level)
202
203        for level in 0..=depth {
204            let mut next_level = Vec::new();
205
206            for (parent_opt, _) in current_level {
207                let num_children = if level == 0 { 1 } else { branching_factor };
208
209                for i in 0..num_children {
210                    let parent_idx = parent_opt.unwrap_or(-1);
211
212                    let (offset, name) = if level == 0 {
213                        (SpatialTransform::identity(), "root".to_string())
214                    } else {
215                        let angle = 2.0 * std::f64::consts::PI * i as f64 / branching_factor as f64;
216                        let offset = Vec3::new(
217                            length * angle.cos() * 0.3,
218                            length * angle.sin() * 0.3,
219                            -length,
220                        );
221                        (
222                            SpatialTransform::translation(offset),
223                            format!("node_{}_{}", body_count, i),
224                        )
225                    };
226
227                    let inertia = SpatialInertia::new(
228                        mass * 0.8_f64.powi(level as i32),
229                        Vec3::new(0.0, 0.0, -length / 2.0),
230                        Mat3::from_diagonal(&Vec3::new(
231                            0.1 * 0.8_f64.powi(level as i32),
232                            0.1 * 0.8_f64.powi(level as i32),
233                            0.0,
234                        )),
235                    );
236
237                    builder = builder.add_revolute_body(&name, parent_idx, offset, inertia);
238
239                    if level < depth {
240                        next_level.push((Some(body_count), level + 1));
241                    }
242
243                    body_count += 1;
244                }
245            }
246
247            current_level = next_level;
248        }
249
250        builder.build()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_random_chain() {
260        let mut generator = WorldGenerator::new(42);
261        let model = generator.random_chain(5, [0.5, 2.0], [0.3, 1.5]);
262        assert_eq!(model.nbodies(), 5);
263        assert_eq!(model.nq, 5);
264        assert_eq!(model.nv, 5);
265    }
266
267    #[test]
268    fn test_random_quadruped() {
269        let mut generator = WorldGenerator::new(42);
270        let model = generator.random_quadruped();
271        // 1 torso + 4 legs * 2 segments = 9 bodies
272        assert_eq!(model.nbodies(), 9);
273        // torso is free (6 DOF) + 8 revolute joints (1 DOF each) = 14 DOF
274        assert_eq!(model.nv, 14);
275    }
276
277    #[test]
278    fn test_platform_with_obstacles() {
279        let mut generator = WorldGenerator::new(42);
280        let model = generator.platform_with_obstacles(10.0, 5, [0.5, 2.0]);
281        // 1 platform + 5 obstacles = 6 bodies
282        assert_eq!(model.nbodies(), 6);
283        // All fixed joints = 0 DOF
284        assert_eq!(model.nv, 0);
285    }
286
287    #[test]
288    fn test_random_tree() {
289        let mut generator = WorldGenerator::new(42);
290        let model = generator.random_tree(2, 2);
291        // 1 root + 2 children + 4 grandchildren = 7 bodies
292        assert_eq!(model.nbodies(), 7);
293    }
294
295    #[test]
296    fn test_deterministic_generation() {
297        let mut gen1 = WorldGenerator::new(123);
298        let mut gen2 = WorldGenerator::new(123);
299
300        let m1 = gen1.random_chain(3, [0.5, 1.5], [0.5, 1.0]);
301        let m2 = gen2.random_chain(3, [0.5, 1.5], [0.5, 1.0]);
302
303        // Same seed should produce same masses
304        for i in 0..3 {
305            assert_eq!(m1.bodies[i].inertia.mass, m2.bodies[i].inertia.mass);
306        }
307    }
308}