|
| 1 | +use coordinate_systems::Robot; |
| 2 | +use linear_algebra::{Isometry3, Orientation3, Point3, Vector2, nalgebra}; |
| 3 | + |
| 4 | +#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 5 | +pub enum SoleSide { |
| 6 | + Left, |
| 7 | + Right, |
| 8 | +} |
| 9 | + |
| 10 | +#[derive(Clone, Copy, Debug)] |
| 11 | +pub struct SoleContact { |
| 12 | + pub min_z: f32, |
| 13 | + pub contact_xy_in_leveled_robot: Vector2<Robot>, |
| 14 | + pub candidate_count: usize, |
| 15 | +} |
| 16 | + |
| 17 | +#[derive(Clone, Copy, Debug)] |
| 18 | +pub struct SupportSelectionParameters { |
| 19 | + pub double_support_deadband: f32, |
| 20 | + pub support_switch_hysteresis: f32, |
| 21 | +} |
| 22 | + |
| 23 | +pub fn estimate_sole_contact<Sole>( |
| 24 | + sole_to_robot: Isometry3<Sole, Robot>, |
| 25 | + roll: f32, |
| 26 | + pitch: f32, |
| 27 | + vertices: &[Point3<Sole>], |
| 28 | + contact_height_epsilon: f32, |
| 29 | +) -> Option<SoleContact> { |
| 30 | + if vertices.is_empty() { |
| 31 | + return None; |
| 32 | + } |
| 33 | + |
| 34 | + struct Level; |
| 35 | + |
| 36 | + let level_to_robot = |
| 37 | + Isometry3::<Level, Robot>::from(Orientation3::from_euler_angles(roll, pitch, 0.0).mirror()); |
| 38 | + let robot_to_level = level_to_robot.inverse(); |
| 39 | + |
| 40 | + let vertices_in_level = vertices |
| 41 | + .iter() |
| 42 | + .map(|vertex| robot_to_level * (sole_to_robot * *vertex)) |
| 43 | + .collect::<Vec<_>>(); |
| 44 | + let min_z = vertices_in_level |
| 45 | + .iter() |
| 46 | + .map(|vertex| vertex.z()) |
| 47 | + .fold(f32::INFINITY, f32::min); |
| 48 | + |
| 49 | + let candidates = vertices_in_level |
| 50 | + .iter() |
| 51 | + .filter(|vertex| vertex.z() <= min_z + contact_height_epsilon) |
| 52 | + .collect::<Vec<_>>(); |
| 53 | + let candidate_count = candidates.len(); |
| 54 | + if candidate_count == 0 { |
| 55 | + return None; |
| 56 | + } |
| 57 | + |
| 58 | + let contact_xy_sum = candidates |
| 59 | + .iter() |
| 60 | + .map(|vertex| vertex.xy().coords().inner) |
| 61 | + .sum::<nalgebra::Vector2<f32>>(); |
| 62 | + let contact_xy = contact_xy_sum / candidate_count as f32; |
| 63 | + |
| 64 | + Some(SoleContact { |
| 65 | + min_z, |
| 66 | + contact_xy_in_leveled_robot: Vector2::wrap(contact_xy), |
| 67 | + candidate_count, |
| 68 | + }) |
| 69 | +} |
| 70 | + |
| 71 | +pub fn select_support_side( |
| 72 | + left_contact: SoleContact, |
| 73 | + right_contact: SoleContact, |
| 74 | + previous_support_side: Option<SoleSide>, |
| 75 | + parameters: SupportSelectionParameters, |
| 76 | +) -> Option<SoleSide> { |
| 77 | + let height_difference = left_contact.min_z - right_contact.min_z; |
| 78 | + |
| 79 | + if height_difference.abs() < parameters.double_support_deadband { |
| 80 | + return None; |
| 81 | + } |
| 82 | + |
| 83 | + match previous_support_side { |
| 84 | + Some(SoleSide::Left) if height_difference < parameters.support_switch_hysteresis => { |
| 85 | + Some(SoleSide::Left) |
| 86 | + } |
| 87 | + Some(SoleSide::Right) if height_difference > -parameters.support_switch_hysteresis => { |
| 88 | + Some(SoleSide::Right) |
| 89 | + } |
| 90 | + _ if height_difference < 0.0 => Some(SoleSide::Left), |
| 91 | + _ => Some(SoleSide::Right), |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +#[cfg(test)] |
| 96 | +mod tests { |
| 97 | + use super::*; |
| 98 | + use linear_algebra::{IntoTransform, nalgebra, point, vector}; |
| 99 | + |
| 100 | + #[derive(Clone, Copy, Debug)] |
| 101 | + struct Sole; |
| 102 | + |
| 103 | + fn vertices() -> [Point3<Sole>; 4] { |
| 104 | + [ |
| 105 | + point![<Sole>, 0.1, 0.05, 0.0], |
| 106 | + point![<Sole>, 0.1, -0.05, 0.0], |
| 107 | + point![<Sole>, -0.1, 0.05, 0.0], |
| 108 | + point![<Sole>, -0.1, -0.05, 0.0], |
| 109 | + ] |
| 110 | + } |
| 111 | + |
| 112 | + #[test] |
| 113 | + fn flat_sole_uses_all_vertices_and_center_contact() { |
| 114 | + let contact = estimate_sole_contact(Isometry3::identity(), 0.0, 0.0, &vertices(), 0.001) |
| 115 | + .expect("flat sole has a contact"); |
| 116 | + |
| 117 | + assert_eq!(contact.candidate_count, 4); |
| 118 | + assert!((contact.contact_xy_in_leveled_robot.x() - 0.0).abs() < 1.0e-6); |
| 119 | + assert!((contact.contact_xy_in_leveled_robot.y() - 0.0).abs() < 1.0e-6); |
| 120 | + } |
| 121 | + |
| 122 | + #[test] |
| 123 | + fn toe_down_pitch_uses_front_vertices() { |
| 124 | + let sole_to_robot = nalgebra::Isometry3::from_parts( |
| 125 | + nalgebra::Translation3::identity(), |
| 126 | + nalgebra::UnitQuaternion::from_euler_angles(0.0, 0.2, 0.0), |
| 127 | + ) |
| 128 | + .framed_transform(); |
| 129 | + |
| 130 | + let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001) |
| 131 | + .expect("toe-down sole has a contact"); |
| 132 | + |
| 133 | + assert_eq!(contact.candidate_count, 2); |
| 134 | + assert!(contact.contact_xy_in_leveled_robot.x() > 0.09); |
| 135 | + } |
| 136 | + |
| 137 | + #[test] |
| 138 | + fn heel_down_pitch_uses_rear_vertices() { |
| 139 | + let sole_to_robot = nalgebra::Isometry3::from_parts( |
| 140 | + nalgebra::Translation3::identity(), |
| 141 | + nalgebra::UnitQuaternion::from_euler_angles(0.0, -0.2, 0.0), |
| 142 | + ) |
| 143 | + .framed_transform(); |
| 144 | + |
| 145 | + let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001) |
| 146 | + .expect("heel-down sole has a contact"); |
| 147 | + |
| 148 | + assert_eq!(contact.candidate_count, 2); |
| 149 | + assert!(contact.contact_xy_in_leveled_robot.x() < -0.09); |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn side_down_roll_uses_left_vertices() { |
| 154 | + let sole_to_robot = nalgebra::Isometry3::from_parts( |
| 155 | + nalgebra::Translation3::identity(), |
| 156 | + nalgebra::UnitQuaternion::from_euler_angles(0.2, 0.0, 0.0), |
| 157 | + ) |
| 158 | + .framed_transform(); |
| 159 | + |
| 160 | + let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001) |
| 161 | + .expect("side-down sole has a contact"); |
| 162 | + |
| 163 | + assert_eq!(contact.candidate_count, 2); |
| 164 | + assert!(contact.contact_xy_in_leveled_robot.y().abs() > 0.04); |
| 165 | + } |
| 166 | + |
| 167 | + #[test] |
| 168 | + fn corner_down_combined_roll_and_pitch_uses_one_vertex() { |
| 169 | + let sole_to_robot = nalgebra::Isometry3::from_parts( |
| 170 | + nalgebra::Translation3::identity(), |
| 171 | + nalgebra::UnitQuaternion::from_euler_angles(0.2, 0.2, 0.0), |
| 172 | + ) |
| 173 | + .framed_transform(); |
| 174 | + |
| 175 | + let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001) |
| 176 | + .expect("corner-down sole has a contact"); |
| 177 | + |
| 178 | + assert_eq!(contact.candidate_count, 1); |
| 179 | + assert!(contact.contact_xy_in_leveled_robot.x() > 0.09); |
| 180 | + assert!(contact.contact_xy_in_leveled_robot.y().abs() > 0.04); |
| 181 | + } |
| 182 | + |
| 183 | + #[test] |
| 184 | + fn double_support_deadband_returns_none() { |
| 185 | + let left = SoleContact { |
| 186 | + min_z: 0.0, |
| 187 | + contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0], |
| 188 | + candidate_count: 4, |
| 189 | + }; |
| 190 | + let right = SoleContact { |
| 191 | + min_z: 0.002, |
| 192 | + contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0], |
| 193 | + candidate_count: 4, |
| 194 | + }; |
| 195 | + |
| 196 | + let side = select_support_side( |
| 197 | + left, |
| 198 | + right, |
| 199 | + None, |
| 200 | + SupportSelectionParameters { |
| 201 | + double_support_deadband: 0.003, |
| 202 | + support_switch_hysteresis: 0.004, |
| 203 | + }, |
| 204 | + ); |
| 205 | + |
| 206 | + assert_eq!(side, None); |
| 207 | + } |
| 208 | + |
| 209 | + #[test] |
| 210 | + fn hysteresis_keeps_previous_side_near_switch_threshold() { |
| 211 | + let left = SoleContact { |
| 212 | + min_z: 0.003, |
| 213 | + contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0], |
| 214 | + candidate_count: 2, |
| 215 | + }; |
| 216 | + let right = SoleContact { |
| 217 | + min_z: 0.0, |
| 218 | + contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0], |
| 219 | + candidate_count: 2, |
| 220 | + }; |
| 221 | + |
| 222 | + let side = select_support_side( |
| 223 | + left, |
| 224 | + right, |
| 225 | + Some(SoleSide::Left), |
| 226 | + SupportSelectionParameters { |
| 227 | + double_support_deadband: 0.001, |
| 228 | + support_switch_hysteresis: 0.004, |
| 229 | + }, |
| 230 | + ); |
| 231 | + |
| 232 | + assert_eq!(side, Some(SoleSide::Left)); |
| 233 | + } |
| 234 | +} |
0 commit comments