Revert "fix: compensate tangential force edge and full-surface cases"
This reverts commit aa08a75aef.
This commit is contained in:
@@ -18,8 +18,6 @@ const ACTIVE_CELL_PEAK_RATIO: f32 = 0.14;
|
|||||||
const MIN_ACTIVE_CELLS: usize = 3;
|
const MIN_ACTIVE_CELLS: usize = 3;
|
||||||
|
|
||||||
const ANCHOR_LERP_ALPHA: f32 = 0.018;
|
const ANCHOR_LERP_ALPHA: f32 = 0.018;
|
||||||
const PATCH_IMMATURE_ANCHOR_ALPHA: f32 = 0.16;
|
|
||||||
const EDGE_GROWTH_ANCHOR_ALPHA: f32 = 0.42;
|
|
||||||
const VECTOR_SMOOTHING_ALPHA: f32 = 0.16;
|
const VECTOR_SMOOTHING_ALPHA: f32 = 0.16;
|
||||||
|
|
||||||
const REPORT_MAGNITUDE_ENTER: f32 = 0.12;
|
const REPORT_MAGNITUDE_ENTER: f32 = 0.12;
|
||||||
@@ -31,21 +29,6 @@ const REPORT_HOLD_FRAMES: usize = 10;
|
|||||||
const ASYMMETRY_WEIGHT: f32 = 1.1;
|
const ASYMMETRY_WEIGHT: f32 = 1.1;
|
||||||
const DRIFT_WEIGHT: f32 = 0.65;
|
const DRIFT_WEIGHT: f32 = 0.65;
|
||||||
const MOTION_WEIGHT: f32 = 0.25;
|
const MOTION_WEIGHT: f32 = 0.25;
|
||||||
const LOCAL_GLOBAL_TREND_WEIGHT: f32 = 0.18;
|
|
||||||
|
|
||||||
const PATCH_MATURE_STABLE_FRAMES: usize = 3;
|
|
||||||
const EDGE_SETTLE_FRAMES: usize = 12;
|
|
||||||
const EDGE_GROWTH_CELL_TOLERANCE: usize = 2;
|
|
||||||
const EDGE_GROWTH_SPAN_TOLERANCE: f32 = 0.08;
|
|
||||||
const EDGE_CLIP_COMPENSATION_WEIGHT: f32 = 0.62;
|
|
||||||
const EDGE_TRANSIENT_DRIFT_GAIN: f32 = 0.22;
|
|
||||||
|
|
||||||
const FULL_SURFACE_ACTIVE_RATIO: f32 = 0.34;
|
|
||||||
const FULL_SURFACE_SPAN_RATIO: f32 = 0.74;
|
|
||||||
const FULL_SURFACE_LOCAL_WEIGHT: f32 = 0.18;
|
|
||||||
const FULL_SURFACE_TREND_WEIGHT: f32 = 1.25;
|
|
||||||
const FULL_SURFACE_DRIFT_WEIGHT: f32 = 0.22;
|
|
||||||
const FULL_SURFACE_MOTION_WEIGHT: f32 = 0.18;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PztSpatialAnalysis {
|
pub struct PztSpatialAnalysis {
|
||||||
@@ -67,11 +50,6 @@ pub struct PztProcessor {
|
|||||||
anchor_cop_y: Option<f32>,
|
anchor_cop_y: Option<f32>,
|
||||||
last_cop_x: Option<f32>,
|
last_cop_x: Option<f32>,
|
||||||
last_cop_y: Option<f32>,
|
last_cop_y: Option<f32>,
|
||||||
contact_frames: usize,
|
|
||||||
stable_patch_frames: usize,
|
|
||||||
last_active_cells: Option<usize>,
|
|
||||||
last_span_rows: Option<f32>,
|
|
||||||
last_span_cols: Option<f32>,
|
|
||||||
smoothed_x: f32,
|
smoothed_x: f32,
|
||||||
smoothed_y: f32,
|
smoothed_y: f32,
|
||||||
report_active: bool,
|
report_active: bool,
|
||||||
@@ -85,19 +63,14 @@ struct ContactStats {
|
|||||||
peak: f32,
|
peak: f32,
|
||||||
active_total: f32,
|
active_total: f32,
|
||||||
active_cells: usize,
|
active_cells: usize,
|
||||||
|
min_row: usize,
|
||||||
|
max_row: usize,
|
||||||
|
min_col: usize,
|
||||||
|
max_col: usize,
|
||||||
cop_x: f32,
|
cop_x: f32,
|
||||||
cop_y: f32,
|
cop_y: f32,
|
||||||
asymmetry_x: f32,
|
asymmetry_x: f32,
|
||||||
asymmetry_y: f32,
|
asymmetry_y: f32,
|
||||||
global_trend_x: f32,
|
|
||||||
global_trend_y: f32,
|
|
||||||
edge_bias_x: f32,
|
|
||||||
edge_bias_y: f32,
|
|
||||||
span_rows: f32,
|
|
||||||
span_cols: f32,
|
|
||||||
coverage: f32,
|
|
||||||
edge_contact: bool,
|
|
||||||
full_surface: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PztProcessor {
|
impl PztProcessor {
|
||||||
@@ -111,11 +84,6 @@ impl PztProcessor {
|
|||||||
anchor_cop_y: None,
|
anchor_cop_y: None,
|
||||||
last_cop_x: None,
|
last_cop_x: None,
|
||||||
last_cop_y: None,
|
last_cop_y: None,
|
||||||
contact_frames: 0,
|
|
||||||
stable_patch_frames: 0,
|
|
||||||
last_active_cells: None,
|
|
||||||
last_span_rows: None,
|
|
||||||
last_span_cols: None,
|
|
||||||
smoothed_x: 0.0,
|
smoothed_x: 0.0,
|
||||||
smoothed_y: 0.0,
|
smoothed_y: 0.0,
|
||||||
report_active: false,
|
report_active: false,
|
||||||
@@ -132,11 +100,6 @@ impl PztProcessor {
|
|||||||
self.anchor_cop_y = None;
|
self.anchor_cop_y = None;
|
||||||
self.last_cop_x = None;
|
self.last_cop_x = None;
|
||||||
self.last_cop_y = None;
|
self.last_cop_y = None;
|
||||||
self.contact_frames = 0;
|
|
||||||
self.stable_patch_frames = 0;
|
|
||||||
self.last_active_cells = None;
|
|
||||||
self.last_span_rows = None;
|
|
||||||
self.last_span_cols = None;
|
|
||||||
self.smoothed_x = 0.0;
|
self.smoothed_x = 0.0;
|
||||||
self.smoothed_y = 0.0;
|
self.smoothed_y = 0.0;
|
||||||
}
|
}
|
||||||
@@ -212,31 +175,6 @@ impl PztProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edge_clip_bias(min_index: usize, max_index: usize, size: usize) -> f32 {
|
|
||||||
let touches_min = min_index == 0;
|
|
||||||
let touches_max = max_index + 1 == size;
|
|
||||||
if touches_min == touches_max {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let span_ratio = (max_index - min_index + 1) as f32 / size as f32;
|
|
||||||
let strength = (1.0 - span_ratio).clamp(0.18, 0.9);
|
|
||||||
if touches_min {
|
|
||||||
strength
|
|
||||||
} else {
|
|
||||||
-strength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn damp_same_direction_bias(value: f32, bias: f32, weight: f32) -> f32 {
|
|
||||||
if bias == 0.0 || value == 0.0 || value.signum() != bias.signum() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let adjusted = (value.abs() - bias.abs() * weight).max(0.0);
|
|
||||||
adjusted * value.signum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_contact_stats(frame: &[f32]) -> Option<ContactStats> {
|
fn compute_contact_stats(frame: &[f32]) -> Option<ContactStats> {
|
||||||
let total = frame.iter().sum::<f32>();
|
let total = frame.iter().sum::<f32>();
|
||||||
if total <= 0.0 {
|
if total <= 0.0 {
|
||||||
@@ -254,24 +192,15 @@ impl PztProcessor {
|
|||||||
let mut active_cells = 0usize;
|
let mut active_cells = 0usize;
|
||||||
let mut weighted_col_sum = 0.0;
|
let mut weighted_col_sum = 0.0;
|
||||||
let mut weighted_row_sum = 0.0;
|
let mut weighted_row_sum = 0.0;
|
||||||
let mut global_col_trend = 0.0;
|
|
||||||
let mut global_row_trend = 0.0;
|
|
||||||
let mut min_row = SENSOR_ROWS;
|
let mut min_row = SENSOR_ROWS;
|
||||||
let mut max_row = 0usize;
|
let mut max_row = 0usize;
|
||||||
let mut min_col = SENSOR_COLS;
|
let mut min_col = SENSOR_COLS;
|
||||||
let mut max_col = 0usize;
|
let mut max_col = 0usize;
|
||||||
let center_col = (SENSOR_COLS - 1) as f32 * 0.5;
|
|
||||||
let center_row = (SENSOR_ROWS - 1) as f32 * 0.5;
|
|
||||||
let half_cols = center_col.max(1.0);
|
|
||||||
let half_rows = center_row.max(1.0);
|
|
||||||
|
|
||||||
for row in 0..SENSOR_ROWS {
|
for row in 0..SENSOR_ROWS {
|
||||||
for col in 0..SENSOR_COLS {
|
for col in 0..SENSOR_COLS {
|
||||||
let index = row * SENSOR_COLS + col;
|
let index = row * SENSOR_COLS + col;
|
||||||
let value = frame[index];
|
let value = frame[index];
|
||||||
global_col_trend += value * ((col as f32 - center_col) / half_cols);
|
|
||||||
global_row_trend += value * ((row as f32 - center_row) / half_rows);
|
|
||||||
|
|
||||||
if value < active_threshold {
|
if value < active_threshold {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -297,18 +226,6 @@ impl PztProcessor {
|
|||||||
let bbox_center_y = (min_row + max_row) as f32 * 0.5;
|
let bbox_center_y = (min_row + max_row) as f32 * 0.5;
|
||||||
let half_width = ((max_col - min_col).max(1) as f32) * 0.5;
|
let half_width = ((max_col - min_col).max(1) as f32) * 0.5;
|
||||||
let half_height = ((max_row - min_row).max(1) as f32) * 0.5;
|
let half_height = ((max_row - min_row).max(1) as f32) * 0.5;
|
||||||
let span_rows = (max_row - min_row + 1) as f32 / SENSOR_ROWS as f32;
|
|
||||||
let span_cols = (max_col - min_col + 1) as f32 / SENSOR_COLS as f32;
|
|
||||||
let coverage = active_cells as f32 / SENSOR_COUNT as f32;
|
|
||||||
let edge_bias_x = Self::edge_clip_bias(min_col, max_col, SENSOR_COLS);
|
|
||||||
let edge_bias_y = Self::edge_clip_bias(min_row, max_row, SENSOR_ROWS);
|
|
||||||
let edge_contact = min_row == 0
|
|
||||||
|| max_row + 1 == SENSOR_ROWS
|
|
||||||
|| min_col == 0
|
|
||||||
|| max_col + 1 == SENSOR_COLS;
|
|
||||||
let full_surface = coverage >= FULL_SURFACE_ACTIVE_RATIO
|
|
||||||
&& span_rows >= FULL_SURFACE_SPAN_RATIO
|
|
||||||
&& span_cols >= FULL_SURFACE_SPAN_RATIO;
|
|
||||||
|
|
||||||
let mut asymmetry_x = 0.0;
|
let mut asymmetry_x = 0.0;
|
||||||
let mut asymmetry_y = 0.0;
|
let mut asymmetry_y = 0.0;
|
||||||
@@ -331,19 +248,14 @@ impl PztProcessor {
|
|||||||
peak,
|
peak,
|
||||||
active_total,
|
active_total,
|
||||||
active_cells,
|
active_cells,
|
||||||
|
min_row,
|
||||||
|
max_row,
|
||||||
|
min_col,
|
||||||
|
max_col,
|
||||||
cop_x,
|
cop_x,
|
||||||
cop_y,
|
cop_y,
|
||||||
asymmetry_x: asymmetry_x / active_total,
|
asymmetry_x: asymmetry_x / active_total,
|
||||||
asymmetry_y: asymmetry_y / active_total,
|
asymmetry_y: asymmetry_y / active_total,
|
||||||
global_trend_x: global_col_trend / total,
|
|
||||||
global_trend_y: global_row_trend / total,
|
|
||||||
edge_bias_x,
|
|
||||||
edge_bias_y,
|
|
||||||
span_rows,
|
|
||||||
span_cols,
|
|
||||||
coverage,
|
|
||||||
edge_contact,
|
|
||||||
full_surface,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,40 +307,6 @@ impl PztProcessor {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_patch_dynamics(&mut self, stats: &ContactStats) -> bool {
|
|
||||||
self.contact_frames += 1;
|
|
||||||
|
|
||||||
let cell_growth = self
|
|
||||||
.last_active_cells
|
|
||||||
.map(|last| stats.active_cells > last + EDGE_GROWTH_CELL_TOLERANCE)
|
|
||||||
.unwrap_or(false);
|
|
||||||
let row_growth = self
|
|
||||||
.last_span_rows
|
|
||||||
.map(|last| stats.span_rows > last + EDGE_GROWTH_SPAN_TOLERANCE)
|
|
||||||
.unwrap_or(false);
|
|
||||||
let col_growth = self
|
|
||||||
.last_span_cols
|
|
||||||
.map(|last| stats.span_cols > last + EDGE_GROWTH_SPAN_TOLERANCE)
|
|
||||||
.unwrap_or(false);
|
|
||||||
let expanding = cell_growth || row_growth || col_growth;
|
|
||||||
|
|
||||||
if expanding {
|
|
||||||
self.stable_patch_frames = 0;
|
|
||||||
} else {
|
|
||||||
self.stable_patch_frames = self.stable_patch_frames.saturating_add(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.last_active_cells = Some(stats.active_cells);
|
|
||||||
self.last_span_rows = Some(stats.span_rows);
|
|
||||||
self.last_span_cols = Some(stats.span_cols);
|
|
||||||
|
|
||||||
expanding
|
|
||||||
}
|
|
||||||
|
|
||||||
fn patch_maturity(&self) -> f32 {
|
|
||||||
(self.stable_patch_frames as f32 / PATCH_MATURE_STABLE_FRAMES as f32).clamp(0.0, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn store_report(&mut self, mut analysis: PztSpatialAnalysis) -> PztSpatialAnalysis {
|
fn store_report(&mut self, mut analysis: PztSpatialAnalysis) -> PztSpatialAnalysis {
|
||||||
analysis.reportable = true;
|
analysis.reportable = true;
|
||||||
self.report_active = true;
|
self.report_active = true;
|
||||||
@@ -492,11 +370,6 @@ impl PztProcessor {
|
|||||||
let Some(stats) = Self::compute_contact_stats(&baseline_subtracted) else {
|
let Some(stats) = Self::compute_contact_stats(&baseline_subtracted) else {
|
||||||
return Ok(self.stabilize_report(Self::weak_contact_analysis()));
|
return Ok(self.stabilize_report(Self::weak_contact_analysis()));
|
||||||
};
|
};
|
||||||
let patch_expanding = self.update_patch_dynamics(&stats);
|
|
||||||
let patch_maturity = self.patch_maturity();
|
|
||||||
let edge_settling =
|
|
||||||
stats.edge_contact && !stats.full_surface && self.contact_frames <= EDGE_SETTLE_FRAMES;
|
|
||||||
let edge_transient = edge_settling && patch_expanding;
|
|
||||||
|
|
||||||
let Some(anchor_x) = self.anchor_cop_x else {
|
let Some(anchor_x) = self.anchor_cop_x else {
|
||||||
self.anchor_cop_x = Some(stats.cop_x);
|
self.anchor_cop_x = Some(stats.cop_x);
|
||||||
@@ -515,75 +388,18 @@ impl PztProcessor {
|
|||||||
let motion_x = stats.cop_x - last_x;
|
let motion_x = stats.cop_x - last_x;
|
||||||
let motion_y = stats.cop_y - last_y;
|
let motion_y = stats.cop_y - last_y;
|
||||||
|
|
||||||
let edge_compensation = if edge_settling {
|
let combined_x = stats.asymmetry_x * ASYMMETRY_WEIGHT
|
||||||
EDGE_CLIP_COMPENSATION_WEIGHT
|
+ drift_x * DRIFT_WEIGHT
|
||||||
} else {
|
+ motion_x * MOTION_WEIGHT;
|
||||||
EDGE_CLIP_COMPENSATION_WEIGHT * 0.65
|
let combined_y = stats.asymmetry_y * ASYMMETRY_WEIGHT
|
||||||
};
|
+ drift_y * DRIFT_WEIGHT
|
||||||
let corrected_asymmetry_x =
|
+ motion_y * MOTION_WEIGHT;
|
||||||
Self::damp_same_direction_bias(stats.asymmetry_x, stats.edge_bias_x, edge_compensation);
|
|
||||||
let corrected_asymmetry_y =
|
|
||||||
Self::damp_same_direction_bias(stats.asymmetry_y, stats.edge_bias_y, edge_compensation);
|
|
||||||
|
|
||||||
let half_cols = ((SENSOR_COLS - 1) as f32 * 0.5).max(1.0);
|
|
||||||
let half_rows = ((SENSOR_ROWS - 1) as f32 * 0.5).max(1.0);
|
|
||||||
let drift_x_norm = drift_x / half_cols;
|
|
||||||
let drift_y_norm = drift_y / half_rows;
|
|
||||||
let motion_x_norm = motion_x / half_cols;
|
|
||||||
let motion_y_norm = motion_y / half_rows;
|
|
||||||
|
|
||||||
let (combined_x, combined_y) = if stats.full_surface {
|
|
||||||
(
|
|
||||||
corrected_asymmetry_x * FULL_SURFACE_LOCAL_WEIGHT
|
|
||||||
+ stats.global_trend_x * FULL_SURFACE_TREND_WEIGHT
|
|
||||||
+ drift_x_norm * FULL_SURFACE_DRIFT_WEIGHT
|
|
||||||
+ motion_x_norm * FULL_SURFACE_MOTION_WEIGHT,
|
|
||||||
corrected_asymmetry_y * FULL_SURFACE_LOCAL_WEIGHT
|
|
||||||
+ stats.global_trend_y * FULL_SURFACE_TREND_WEIGHT
|
|
||||||
+ drift_y_norm * FULL_SURFACE_DRIFT_WEIGHT
|
|
||||||
+ motion_y_norm * FULL_SURFACE_MOTION_WEIGHT,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let drift_gain = if edge_settling {
|
|
||||||
EDGE_TRANSIENT_DRIFT_GAIN
|
|
||||||
} else {
|
|
||||||
patch_maturity.max(0.35)
|
|
||||||
};
|
|
||||||
let motion_gain = if edge_transient {
|
|
||||||
EDGE_TRANSIENT_DRIFT_GAIN.max(0.35)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let local_trend_gain = if stats.edge_contact {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
((stats.coverage - 0.16) / 0.18).clamp(0.0, 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
corrected_asymmetry_x * ASYMMETRY_WEIGHT
|
|
||||||
+ drift_x * DRIFT_WEIGHT * drift_gain
|
|
||||||
+ motion_x * MOTION_WEIGHT * motion_gain
|
|
||||||
+ stats.global_trend_x * LOCAL_GLOBAL_TREND_WEIGHT * local_trend_gain,
|
|
||||||
corrected_asymmetry_y * ASYMMETRY_WEIGHT
|
|
||||||
+ drift_y * DRIFT_WEIGHT * drift_gain
|
|
||||||
+ motion_y * MOTION_WEIGHT * motion_gain
|
|
||||||
+ stats.global_trend_y * LOCAL_GLOBAL_TREND_WEIGHT * local_trend_gain,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.smoothed_x += (combined_x - self.smoothed_x) * VECTOR_SMOOTHING_ALPHA;
|
self.smoothed_x += (combined_x - self.smoothed_x) * VECTOR_SMOOTHING_ALPHA;
|
||||||
self.smoothed_y += (combined_y - self.smoothed_y) * VECTOR_SMOOTHING_ALPHA;
|
self.smoothed_y += (combined_y - self.smoothed_y) * VECTOR_SMOOTHING_ALPHA;
|
||||||
|
|
||||||
let anchor_alpha = if edge_settling {
|
self.anchor_cop_x = Some(anchor_x + drift_x * ANCHOR_LERP_ALPHA);
|
||||||
EDGE_GROWTH_ANCHOR_ALPHA
|
self.anchor_cop_y = Some(anchor_y + drift_y * ANCHOR_LERP_ALPHA);
|
||||||
} else if patch_maturity < 1.0 && !stats.full_surface {
|
|
||||||
PATCH_IMMATURE_ANCHOR_ALPHA
|
|
||||||
} else {
|
|
||||||
ANCHOR_LERP_ALPHA
|
|
||||||
};
|
|
||||||
self.anchor_cop_x = Some(anchor_x + drift_x * anchor_alpha);
|
|
||||||
self.anchor_cop_y = Some(anchor_y + drift_y * anchor_alpha);
|
|
||||||
self.last_cop_x = Some(stats.cop_x);
|
self.last_cop_x = Some(stats.cop_x);
|
||||||
self.last_cop_y = Some(stats.cop_y);
|
self.last_cop_y = Some(stats.cop_y);
|
||||||
|
|
||||||
@@ -591,33 +407,15 @@ impl PztProcessor {
|
|||||||
let planar_y = -self.smoothed_y;
|
let planar_y = -self.smoothed_y;
|
||||||
let (angle_deg, magnitude) = Self::compute_vector_angle(planar_x, planar_y);
|
let (angle_deg, magnitude) = Self::compute_vector_angle(planar_x, planar_y);
|
||||||
|
|
||||||
let activity = stats.coverage.clamp(0.0, 1.0);
|
let active_span_rows = (stats.max_row - stats.min_row + 1) as f32 / SENSOR_ROWS as f32;
|
||||||
let span = ((stats.span_rows + stats.span_cols) * 0.5).clamp(0.0, 1.0);
|
let active_span_cols = (stats.max_col - stats.min_col + 1) as f32 / SENSOR_COLS as f32;
|
||||||
|
let activity = (stats.active_cells as f32 / SENSOR_COUNT as f32).clamp(0.0, 1.0);
|
||||||
|
let span = ((active_span_rows + active_span_cols) * 0.5).clamp(0.0, 1.0);
|
||||||
let pressure_ratio = (stats.active_total / stats.total.max(1.0)).clamp(0.0, 1.0);
|
let pressure_ratio = (stats.active_total / stats.total.max(1.0)).clamp(0.0, 1.0);
|
||||||
let peak_ratio =
|
let peak_ratio =
|
||||||
(stats.peak / (stats.total / stats.active_cells as f32 + 1.0)).clamp(0.0, 1.0);
|
(stats.peak / (stats.total / stats.active_cells as f32 + 1.0)).clamp(0.0, 1.0);
|
||||||
let trend_strength = (stats.global_trend_x * stats.global_trend_x
|
|
||||||
+ stats.global_trend_y * stats.global_trend_y)
|
|
||||||
.sqrt()
|
|
||||||
.clamp(0.0, 1.0);
|
|
||||||
let edge_penalty = if edge_settling {
|
|
||||||
0.72
|
|
||||||
} else if stats.edge_contact && !stats.full_surface {
|
|
||||||
0.9
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let full_surface_bonus = if stats.full_surface { 0.12 } else { 0.0 };
|
|
||||||
let trend_bonus = if stats.full_surface {
|
|
||||||
trend_strength * 0.28
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let confidence =
|
let confidence =
|
||||||
(((activity * 0.35) + (span * 0.2) + (pressure_ratio * 0.3) + (peak_ratio * 0.15))
|
((activity * 0.35) + (span * 0.2) + (pressure_ratio * 0.3) + (peak_ratio * 0.15))
|
||||||
* edge_penalty
|
|
||||||
+ full_surface_bonus
|
|
||||||
+ trend_bonus)
|
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
|
|
||||||
Ok(self.stabilize_report(PztSpatialAnalysis {
|
Ok(self.stabilize_report(PztSpatialAnalysis {
|
||||||
@@ -662,32 +460,6 @@ mod tests {
|
|||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_surface_frame(
|
|
||||||
mut value_at: impl FnMut(usize, usize) -> f32,
|
|
||||||
) -> [f32; SENSOR_ROWS * SENSOR_COLS] {
|
|
||||||
let mut frame = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
|
||||||
for row in 0..SENSOR_ROWS {
|
|
||||||
for col in 0..SENSOR_COLS {
|
|
||||||
frame[index(row, col)] = value_at(row, col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frame
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_left_edge_patch(width: usize, value: f32) -> [f32; SENSOR_ROWS * SENSOR_COLS] {
|
|
||||||
let mut frame = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
|
||||||
for row in 4..=7 {
|
|
||||||
for col in 0..width.min(SENSOR_COLS) {
|
|
||||||
frame[index(row, col)] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frame
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_rightward(angle_deg: f32) -> bool {
|
|
||||||
angle_deg <= 45.0 || angle_deg >= 315.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn idle_frame_does_not_report_contact() {
|
fn idle_frame_does_not_report_contact() {
|
||||||
let mut processor = PztProcessor::new();
|
let mut processor = PztProcessor::new();
|
||||||
@@ -724,7 +496,7 @@ mod tests {
|
|||||||
assert!(analysis.contact_active);
|
assert!(analysis.contact_active);
|
||||||
assert!(analysis.reportable);
|
assert!(analysis.reportable);
|
||||||
assert!(analysis.magnitude > 0.0);
|
assert!(analysis.magnitude > 0.0);
|
||||||
assert!(is_rightward(analysis.angle_deg));
|
assert!(analysis.angle_deg <= 45.0 || analysis.angle_deg >= 315.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -752,76 +524,4 @@ mod tests {
|
|||||||
let analysis = processor.get_pzt_analysis(&weak).unwrap();
|
let analysis = processor.get_pzt_analysis(&weak).unwrap();
|
||||||
assert!(analysis.reportable);
|
assert!(analysis.reportable);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_patch_growth_does_not_create_false_inward_force() {
|
|
||||||
let mut processor = PztProcessor::new();
|
|
||||||
let baseline = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
|
||||||
let _ = processor.get_pzt_analysis(&baseline).unwrap();
|
|
||||||
|
|
||||||
let mut analysis = processor
|
|
||||||
.get_pzt_analysis(&make_left_edge_patch(1, 180.0))
|
|
||||||
.unwrap();
|
|
||||||
for width in [1, 2, 3, 4, 4, 4, 4, 4, 4, 4] {
|
|
||||||
analysis = processor
|
|
||||||
.get_pzt_analysis(&make_left_edge_patch(width, 180.0))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(analysis.contact_active);
|
|
||||||
assert!(!analysis.reportable || analysis.magnitude < 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_to_inner_motion_can_still_report_inward_direction() {
|
|
||||||
let mut processor = PztProcessor::new();
|
|
||||||
let baseline = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
|
||||||
let _ = processor.get_pzt_analysis(&baseline).unwrap();
|
|
||||||
for _ in 0..8 {
|
|
||||||
let _ = processor
|
|
||||||
.get_pzt_analysis(&make_left_edge_patch(2, 180.0))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let inward = make_frame(&[
|
|
||||||
(4, 1, 120.0),
|
|
||||||
(4, 2, 180.0),
|
|
||||||
(4, 3, 280.0),
|
|
||||||
(5, 1, 120.0),
|
|
||||||
(5, 2, 180.0),
|
|
||||||
(5, 3, 280.0),
|
|
||||||
(6, 1, 120.0),
|
|
||||||
(6, 2, 180.0),
|
|
||||||
(6, 3, 280.0),
|
|
||||||
(7, 1, 120.0),
|
|
||||||
(7, 2, 180.0),
|
|
||||||
(7, 3, 280.0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut analysis = processor.get_pzt_analysis(&inward).unwrap();
|
|
||||||
for _ in 0..8 {
|
|
||||||
analysis = processor.get_pzt_analysis(&inward).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(analysis.contact_active);
|
|
||||||
assert!(analysis.reportable);
|
|
||||||
assert!(is_rightward(analysis.angle_deg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn full_surface_press_uses_global_pressure_trend() {
|
|
||||||
let mut processor = PztProcessor::new();
|
|
||||||
let baseline = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
|
||||||
let full_surface = make_surface_frame(|_, col| 70.0 + col as f32 * 16.0);
|
|
||||||
|
|
||||||
let _ = processor.get_pzt_analysis(&baseline).unwrap();
|
|
||||||
let mut analysis = processor.get_pzt_analysis(&full_surface).unwrap();
|
|
||||||
for _ in 0..8 {
|
|
||||||
analysis = processor.get_pzt_analysis(&full_surface).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(analysis.contact_active);
|
|
||||||
assert!(analysis.reportable);
|
|
||||||
assert!(is_rightward(analysis.angle_deg));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user