Skip to main content

phyz_world/
sensor.rs

1//! Sensor models for extracting observations from simulation state.
2
3use phyz_model::{Model, State};
4
5/// Sensor types for extracting observations from simulation.
6#[derive(Debug, Clone)]
7pub enum Sensor {
8    /// Joint position and velocity sensor.
9    JointState { joint_idx: usize },
10    /// Body linear acceleration in world frame.
11    BodyAccel { body_idx: usize },
12    /// Body angular velocity in body frame.
13    BodyAngularVel { body_idx: usize },
14    /// Reaction force/torque at body.
15    ForceTorque { body_idx: usize },
16    /// Distance to nearest obstacle (placeholder - requires collision detection).
17    Rangefinder { body_idx: usize, max_dist: f64 },
18    /// IMU: acceleration + angular velocity in body frame.
19    Imu { body_idx: usize },
20    /// Snapshot of body transform.
21    FrameCapture { body_idx: usize },
22}
23
24/// Output from a sensor reading.
25#[derive(Debug, Clone)]
26pub struct SensorOutput {
27    /// Sensor identifier (index in sensor array).
28    pub sensor_id: usize,
29    /// Simulation time when reading was taken.
30    pub timestamp: f64,
31    /// Flattened sensor data.
32    pub data: Vec<f64>,
33}
34
35impl Sensor {
36    /// Read sensor output from current state.
37    ///
38    /// Returns a SensorOutput with sensor_id set to 0 (caller should update).
39    pub fn read(&self, model: &Model, state: &State, sensor_id: usize) -> SensorOutput {
40        let data = match self {
41            Sensor::JointState { joint_idx } => {
42                let q_off = model.q_offsets[*joint_idx];
43                let v_off = model.v_offsets[*joint_idx];
44                let ndof = model.joints[*joint_idx].ndof();
45
46                let mut out = Vec::with_capacity(ndof * 2);
47                for i in 0..ndof {
48                    out.push(state.q[q_off + i]);
49                }
50                for i in 0..ndof {
51                    out.push(state.v[v_off + i]);
52                }
53                out
54            }
55            Sensor::BodyAccel { body_idx: _ } => {
56                // Acceleration = d²x/dt² ≈ (v_next - v_curr) / dt
57                // This requires storing previous velocity or computing numerically.
58                // For now, return zero (placeholder).
59                vec![0.0, 0.0, 0.0]
60            }
61            Sensor::BodyAngularVel { body_idx } => {
62                // Extract angular velocity from body spatial velocity
63                // v_spatial = S * qd where S is motion subspace
64                // For now, approximate from joint velocities
65                if *body_idx >= model.nbodies() {
66                    vec![0.0, 0.0, 0.0]
67                } else {
68                    let joint_idx = model.bodies[*body_idx].joint_idx;
69                    let v_off = model.v_offsets[joint_idx];
70                    let joint = &model.joints[joint_idx];
71
72                    match joint.ndof() {
73                        1 => {
74                            // Single revolute joint: angular velocity = axis * qd
75                            let qd = state.v[v_off];
76                            let w = joint.axis * qd;
77                            vec![w.x, w.y, w.z]
78                        }
79                        3 | 6 => {
80                            // Spherical or free joint: angular velocity is directly in state
81                            vec![state.v[v_off], state.v[v_off + 1], state.v[v_off + 2]]
82                        }
83                        _ => vec![0.0, 0.0, 0.0],
84                    }
85                }
86            }
87            Sensor::ForceTorque { body_idx: _ } => {
88                // Reaction forces require inverse dynamics (RNEA).
89                // Placeholder: return zeros.
90                vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
91            }
92            Sensor::Rangefinder {
93                body_idx: _,
94                max_dist,
95            } => {
96                // Requires collision detection to find nearest obstacle.
97                // Placeholder: return max distance.
98                vec![*max_dist]
99            }
100            Sensor::Imu { body_idx } => {
101                // IMU = acceleration (3) + angular velocity (3)
102                // Placeholder: return zeros for acceleration, compute angular velocity
103                let mut imu_data = vec![0.0, 0.0, 0.0]; // acceleration
104
105                if *body_idx < model.nbodies() {
106                    let joint_idx = model.bodies[*body_idx].joint_idx;
107                    let v_off = model.v_offsets[joint_idx];
108                    let joint = &model.joints[joint_idx];
109
110                    let angular_vel = match joint.ndof() {
111                        1 => {
112                            let qd = state.v[v_off];
113                            let w = joint.axis * qd;
114                            vec![w.x, w.y, w.z]
115                        }
116                        3 | 6 => {
117                            vec![state.v[v_off], state.v[v_off + 1], state.v[v_off + 2]]
118                        }
119                        _ => vec![0.0, 0.0, 0.0],
120                    };
121                    imu_data.extend(angular_vel);
122                } else {
123                    imu_data.extend(vec![0.0, 0.0, 0.0]);
124                }
125
126                imu_data
127            }
128            Sensor::FrameCapture { body_idx } => {
129                // Return body transform as [x, y, z, qw, qx, qy, qz]
130                if *body_idx < state.body_xform.len() {
131                    let xform = &state.body_xform[*body_idx];
132                    let pos = xform.pos;
133
134                    // Convert rotation matrix to quaternion
135                    // This is a simplified conversion - should use proper matrix->quat
136                    let quat = mat3_to_quat(&xform.rot);
137
138                    vec![pos.x, pos.y, pos.z, quat.0, quat.1, quat.2, quat.3]
139                } else {
140                    vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
141                }
142            }
143        };
144
145        SensorOutput {
146            sensor_id,
147            timestamp: state.time,
148            data,
149        }
150    }
151
152    /// Get expected output dimension for this sensor.
153    pub fn output_dim(&self) -> usize {
154        match self {
155            Sensor::JointState { .. } => 2, // q + v (multiplied by ndof at runtime)
156            Sensor::BodyAccel { .. } => 3,
157            Sensor::BodyAngularVel { .. } => 3,
158            Sensor::ForceTorque { .. } => 6,
159            Sensor::Rangefinder { .. } => 1,
160            Sensor::Imu { .. } => 6,          // accel (3) + gyro (3)
161            Sensor::FrameCapture { .. } => 7, // pos (3) + quat (4)
162        }
163    }
164}
165
166/// Helper function to convert rotation matrix to quaternion.
167/// Returns (w, x, y, z).
168fn mat3_to_quat(mat: &phyz_math::Mat3) -> (f64, f64, f64, f64) {
169    // Shepperd's method for numerical stability
170    let trace = mat[(0, 0)] + mat[(1, 1)] + mat[(2, 2)];
171
172    if trace > 0.0 {
173        let s = (trace + 1.0).sqrt() * 2.0;
174        let w = 0.25 * s;
175        let x = (mat[(2, 1)] - mat[(1, 2)]) / s;
176        let y = (mat[(0, 2)] - mat[(2, 0)]) / s;
177        let z = (mat[(1, 0)] - mat[(0, 1)]) / s;
178        (w, x, y, z)
179    } else if mat[(0, 0)] > mat[(1, 1)] && mat[(0, 0)] > mat[(2, 2)] {
180        let s = (1.0 + mat[(0, 0)] - mat[(1, 1)] - mat[(2, 2)]).sqrt() * 2.0;
181        let w = (mat[(2, 1)] - mat[(1, 2)]) / s;
182        let x = 0.25 * s;
183        let y = (mat[(0, 1)] + mat[(1, 0)]) / s;
184        let z = (mat[(0, 2)] + mat[(2, 0)]) / s;
185        (w, x, y, z)
186    } else if mat[(1, 1)] > mat[(2, 2)] {
187        let s = (1.0 + mat[(1, 1)] - mat[(0, 0)] - mat[(2, 2)]).sqrt() * 2.0;
188        let w = (mat[(0, 2)] - mat[(2, 0)]) / s;
189        let x = (mat[(0, 1)] + mat[(1, 0)]) / s;
190        let y = 0.25 * s;
191        let z = (mat[(1, 2)] + mat[(2, 1)]) / s;
192        (w, x, y, z)
193    } else {
194        let s = (1.0 + mat[(2, 2)] - mat[(0, 0)] - mat[(1, 1)]).sqrt() * 2.0;
195        let w = (mat[(1, 0)] - mat[(0, 1)]) / s;
196        let x = (mat[(0, 2)] + mat[(2, 0)]) / s;
197        let y = (mat[(1, 2)] + mat[(2, 1)]) / s;
198        let z = 0.25 * s;
199        (w, x, y, z)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use phyz_math::{SpatialInertia, SpatialTransform, Vec3};
207    use phyz_model::ModelBuilder;
208
209    #[test]
210    fn test_joint_state_sensor() {
211        let model = ModelBuilder::new()
212            .add_revolute_body(
213                "link",
214                -1,
215                SpatialTransform::identity(),
216                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
217            )
218            .build();
219
220        let mut state = model.default_state();
221        state.q[0] = 0.5;
222        state.v[0] = 1.0;
223
224        let sensor = Sensor::JointState { joint_idx: 0 };
225        let output = sensor.read(&model, &state, 0);
226
227        assert_eq!(output.data.len(), 2);
228        assert_eq!(output.data[0], 0.5);
229        assert_eq!(output.data[1], 1.0);
230    }
231
232    #[test]
233    fn test_body_angular_vel_sensor() {
234        let model = ModelBuilder::new()
235            .add_revolute_body(
236                "link",
237                -1,
238                SpatialTransform::identity(),
239                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
240            )
241            .build();
242
243        let mut state = model.default_state();
244        state.v[0] = 2.0;
245
246        let sensor = Sensor::BodyAngularVel { body_idx: 0 };
247        let output = sensor.read(&model, &state, 0);
248
249        assert_eq!(output.data.len(), 3);
250        // Angular velocity should be along Z axis (joint axis) with magnitude 2.0
251        assert!((output.data[2] - 2.0).abs() < 1e-10);
252    }
253
254    #[test]
255    fn test_imu_sensor() {
256        let model = ModelBuilder::new()
257            .add_revolute_body(
258                "link",
259                -1,
260                SpatialTransform::identity(),
261                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
262            )
263            .build();
264
265        let state = model.default_state();
266        let sensor = Sensor::Imu { body_idx: 0 };
267        let output = sensor.read(&model, &state, 0);
268
269        assert_eq!(output.data.len(), 6); // 3 accel + 3 gyro
270    }
271
272    #[test]
273    fn test_frame_capture_sensor() {
274        let model = ModelBuilder::new()
275            .add_revolute_body(
276                "link",
277                -1,
278                SpatialTransform::identity(),
279                SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
280            )
281            .build();
282
283        let state = model.default_state();
284        let sensor = Sensor::FrameCapture { body_idx: 0 };
285        let output = sensor.read(&model, &state, 0);
286
287        assert_eq!(output.data.len(), 7); // 3 pos + 4 quat
288        // Quaternion should be approximately identity [1, 0, 0, 0]
289        assert!((output.data[3] - 1.0).abs() < 0.1); // qw close to 1
290    }
291}