Skip to main content

phyz_world/
world.rs

1//! World container that combines model, state, sensors, and tendons.
2
3use crate::{Sensor, SensorOutput, Tendon};
4use phyz_model::{Model, State};
5
6/// A complete simulation world with sensors and actuators.
7pub struct World {
8    /// The kinematic model.
9    pub model: Model,
10    /// Current simulation state.
11    pub state: State,
12    /// Sensors attached to the world.
13    pub sensors: Vec<Sensor>,
14    /// History of sensor readings (one vec per timestep).
15    pub sensor_history: Vec<Vec<SensorOutput>>,
16    /// Tendons (cable actuators) in the world.
17    pub tendons: Vec<Tendon>,
18}
19
20impl World {
21    /// Create a new world from a model.
22    pub fn new(model: Model) -> Self {
23        let state = model.default_state();
24        Self {
25            model,
26            state,
27            sensors: Vec::new(),
28            sensor_history: Vec::new(),
29            tendons: Vec::new(),
30        }
31    }
32
33    /// Create a new world with initial state.
34    pub fn with_state(model: Model, state: State) -> Self {
35        Self {
36            model,
37            state,
38            sensors: Vec::new(),
39            sensor_history: Vec::new(),
40            tendons: Vec::new(),
41        }
42    }
43
44    /// Add a sensor to the world.
45    pub fn add_sensor(&mut self, sensor: Sensor) {
46        self.sensors.push(sensor);
47    }
48
49    /// Add a tendon to the world.
50    pub fn add_tendon(&mut self, tendon: Tendon) {
51        if !tendon.is_valid(&self.model) {
52            panic!("Invalid tendon: path contains out-of-bounds body indices");
53        }
54        self.tendons.push(tendon);
55    }
56
57    /// Step the simulation forward using a custom step function.
58    ///
59    /// The step_fn should advance the state by one timestep.
60    /// Records sensor outputs after the step.
61    pub fn step<F>(&mut self, step_fn: F)
62    where
63        F: FnOnce(&Model, &mut State),
64    {
65        // Apply tendon forces as external forces
66        self.apply_tendon_forces();
67
68        // Step simulation
69        step_fn(&self.model, &mut self.state);
70
71        // Record sensor outputs
72        let readings = self.read_sensors();
73        self.sensor_history.push(readings);
74    }
75
76    /// Read all sensors at the current state.
77    fn read_sensors(&self) -> Vec<SensorOutput> {
78        self.sensors
79            .iter()
80            .enumerate()
81            .map(|(id, sensor)| sensor.read(&self.model, &self.state, id))
82            .collect()
83    }
84
85    /// Apply tendon forces to state as external forces.
86    ///
87    /// Modifies state.ctrl to include tendon contributions.
88    fn apply_tendon_forces(&mut self) {
89        for tendon in &self.tendons {
90            let forces = tendon.compute_forces(&self.state);
91
92            // Convert spatial forces to joint torques
93            // This is a simplified approach - proper implementation would use
94            // Jacobian transpose: tau = J^T * f
95            for (body_idx, _force) in forces {
96                if body_idx < self.model.nbodies() {
97                    let joint_idx = self.model.bodies[body_idx].joint_idx;
98                    let v_off = self.model.v_offsets[joint_idx];
99
100                    // For now, just add a small contribution
101                    // Proper implementation needs geometric Jacobian
102                    if v_off < self.state.ctrl.len() {
103                        // Placeholder: proportional to stretch
104                        let current_len = tendon.current_length(&self.state);
105                        let stretch = current_len - tendon.rest_length;
106                        let torque = -tendon.stiffness * stretch * 0.1; // scaled contribution
107                        self.state.ctrl[v_off] += torque;
108                    }
109                }
110            }
111        }
112    }
113
114    /// Get the most recent sensor readings.
115    pub fn latest_sensor_readings(&self) -> Option<&Vec<SensorOutput>> {
116        self.sensor_history.last()
117    }
118
119    /// Clear sensor history.
120    pub fn clear_history(&mut self) {
121        self.sensor_history.clear();
122    }
123
124    /// Get number of timesteps recorded.
125    pub fn history_len(&self) -> usize {
126        self.sensor_history.len()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use phyz_math::{SpatialInertia, SpatialTransform, Vec3};
134    use phyz_model::ModelBuilder;
135
136    #[test]
137    fn test_world_creation() {
138        let model = ModelBuilder::new()
139            .add_revolute_body(
140                "link",
141                -1,
142                SpatialTransform::identity(),
143                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
144            )
145            .build();
146
147        let world = World::new(model);
148        assert_eq!(world.sensors.len(), 0);
149        assert_eq!(world.tendons.len(), 0);
150        assert_eq!(world.history_len(), 0);
151    }
152
153    #[test]
154    fn test_world_with_sensor() {
155        let model = ModelBuilder::new()
156            .add_revolute_body(
157                "link",
158                -1,
159                SpatialTransform::identity(),
160                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
161            )
162            .build();
163
164        let mut world = World::new(model);
165        world.add_sensor(Sensor::JointState { joint_idx: 0 });
166
167        assert_eq!(world.sensors.len(), 1);
168    }
169
170    #[test]
171    fn test_world_step_with_sensors() {
172        let model = ModelBuilder::new()
173            .dt(0.01)
174            .add_revolute_body(
175                "link",
176                -1,
177                SpatialTransform::identity(),
178                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
179            )
180            .build();
181
182        let mut world = World::new(model);
183        world.add_sensor(Sensor::JointState { joint_idx: 0 });
184
185        // Step a few times with a dummy step function
186        for _ in 0..5 {
187            world.step(|_model, state| {
188                state.time += 0.01;
189            });
190        }
191
192        assert_eq!(world.history_len(), 5);
193        assert!(world.latest_sensor_readings().is_some());
194    }
195
196    #[test]
197    fn test_world_with_tendon() {
198        let model = ModelBuilder::new()
199            .add_revolute_body(
200                "link1",
201                -1,
202                SpatialTransform::identity(),
203                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
204            )
205            .add_revolute_body(
206                "link2",
207                0,
208                SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.0)),
209                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
210            )
211            .build();
212
213        let mut world = World::new(model);
214        let tendon = Tendon::new(vec![0, 1], 100.0, 1.0, 10.0);
215        world.add_tendon(tendon);
216
217        assert_eq!(world.tendons.len(), 1);
218    }
219
220    #[test]
221    #[should_panic(expected = "Invalid tendon")]
222    fn test_world_invalid_tendon() {
223        let model = ModelBuilder::new()
224            .add_revolute_body(
225                "link",
226                -1,
227                SpatialTransform::identity(),
228                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
229            )
230            .build();
231
232        let mut world = World::new(model);
233        // Try to add tendon with invalid body index
234        let tendon = Tendon::new(vec![0, 99], 100.0, 1.0, 10.0);
235        world.add_tendon(tendon);
236    }
237}