use crate::convex::{ContactProblem, ContactSolution, ContactSolverConfig};
use phyz_math::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactRegime {
Separating,
Sticking,
Sliding,
}
pub fn classify(
problem: &ContactProblem,
solution: &ContactSolution,
tol: f64,
) -> Vec<ContactRegime> {
classify_impulses(problem, &solution.impulses, tol)
}
pub(crate) fn classify_impulses(
problem: &ContactProblem,
impulses: &[Vec3],
tol: f64,
) -> Vec<ContactRegime> {
impulses
.iter()
.zip(&problem.rows)
.map(|(f, row)| {
if f.x <= tol {
return ContactRegime::Separating;
}
let ft = (f.y * f.y + f.z * f.z).sqrt();
let limit = row.mu * f.x;
if limit <= 0.0 || ft >= limit - tol * f.x.max(1.0) {
ContactRegime::Sliding
} else {
ContactRegime::Sticking
}
})
.collect()
}
pub struct FixedPointSensitivity {
n: usize,
inv: Vec<f64>,
slide: Vec<Option<SlideTangent>>,
regimes: Vec<ContactRegime>,
}
#[derive(Debug, Clone, Copy)]
pub struct SlideTangent {
pub map: [[f64; 2]; 2],
pub t_rel: [f64; 2],
}
impl FixedPointSensitivity {
pub fn at(
problem: &ContactProblem,
solution: &ContactSolution,
config: &ContactSolverConfig,
) -> Option<Self> {
if !solution.converged {
return None;
}
let n = problem.n;
let dim = 3 * n;
if n == 0 {
return Some(Self {
n,
inv: Vec::new(),
slide: Vec::new(),
regimes: Vec::new(),
});
}
let regimes = classify(problem, solution, 1e-7);
let mut k = kkt_matrix(problem, config, ®imes, &solution.impulses);
let a = &problem.delassus;
let mut slide: Vec<Option<SlideTangent>> = vec![None; n];
for c in 0..n {
if regimes[c] != ContactRegime::Sliding {
continue;
}
let base = 3 * c;
let f = solution.impulses[c];
let ft = (f.y * f.y + f.z * f.z).sqrt();
if ft <= 1e-14 {
continue;
}
let reg = crate::convex::regularization_diag(problem, c, config);
let m = [
[
a[(base + 1) * dim + base + 1] + reg[1],
a[(base + 1) * dim + base + 2],
],
[
a[(base + 2) * dim + base + 1],
a[(base + 2) * dim + base + 2] + reg[2],
],
];
let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
if det.abs() < 1e-30 {
continue;
}
let minv = [
[m[1][1] / det, -m[0][1] / det],
[-m[1][0] / det, m[0][0] / det],
];
let flat = flat_impulses(&solution.impulses);
let mut r = [0.0f64; 2];
for (i, ri) in r.iter_mut().enumerate() {
let row = base + 1 + i;
let mut acc = problem.free_velocity[row];
for (col, fc) in flat.iter().enumerate() {
acc += a[row * dim + col] * fc;
}
acc += reg[1 + i] * flat[row];
acc -= m[i][0] * f.y + m[i][1] * f.z;
*ri = acc;
}
let t_star = [
-(minv[0][0] * r[0] + minv[0][1] * r[1]),
-(minv[1][0] * r[0] + minv[1][1] * r[1]),
];
let t_norm = (t_star[0] * t_star[0] + t_star[1] * t_star[1]).sqrt();
if t_norm <= 1e-14 {
continue;
}
let that = [t_star[0] / t_norm, t_star[1] / t_norm];
let s = (ft / t_norm).min(1.0);
let p = [
[1.0 - that[0] * that[0], -that[0] * that[1]],
[-that[1] * that[0], 1.0 - that[1] * that[1]],
];
let mut cmap = [[0.0f64; 2]; 2];
for i in 0..2 {
for j in 0..2 {
cmap[i][j] = s * (p[i][0] * minv[0][j] + p[i][1] * minv[1][j]);
}
}
let mu = problem.rows[c].mu;
for i in 0..2 {
let row = base + 1 + i;
for col in 0..dim {
let in_own_t = col == base + 1 || col == base + 2;
k[row * dim + col] = if in_own_t {
if col == row { 1.0 } else { 0.0 }
} else {
cmap[i][0] * a[(base + 1) * dim + col]
+ cmap[i][1] * a[(base + 2) * dim + col]
};
}
k[row * dim + base] -= mu * that[i];
}
slide[c] = Some(SlideTangent {
map: cmap,
t_rel: [t_star[0] - f.y, t_star[1] - f.z],
});
}
let mut rhs = vec![0.0; dim * dim];
for i in 0..dim {
rhs[i * dim + i] = -1.0;
}
solve_dense(&mut k, &mut rhs, dim, dim)?;
Some(Self {
n,
inv: rhs,
slide,
regimes,
})
}
pub fn slide_tangent(&self, c: usize) -> Option<&SlideTangent> {
self.slide[c].as_ref()
}
#[allow(clippy::needless_range_loop)]
pub fn apply(&self, d_stationarity: &[f64], d_mt_rel: &[[f64; 2]]) -> Vec<Vec3> {
let n = self.n;
let dim = 3 * n;
let mut dres = vec![0.0; dim];
for c in 0..n {
let base = 3 * c;
if self.regimes[c] == ContactRegime::Separating {
continue;
}
match &self.slide[c] {
None => {
dres[base] = d_stationarity[base];
if self.regimes[c] != ContactRegime::Sliding {
dres[base + 1] = d_stationarity[base + 1];
dres[base + 2] = d_stationarity[base + 2];
}
}
Some(st) => {
dres[base] = d_stationarity[base];
let dr = [
d_stationarity[base + 1] + d_mt_rel[c][0],
d_stationarity[base + 2] + d_mt_rel[c][1],
];
dres[base + 1] = st.map[0][0] * dr[0] + st.map[0][1] * dr[1];
dres[base + 2] = st.map[1][0] * dr[0] + st.map[1][1] * dr[1];
}
}
}
let mut out = vec![Vec3::zeros(); n];
for c in 0..n {
let mut d = [0.0; 3];
for (r, dr) in d.iter_mut().enumerate() {
let row = 3 * c + r;
let mut acc = 0.0;
for (col, dc) in dres.iter().enumerate() {
acc += self.inv[row * dim + col] * dc;
}
*dr = acc;
}
out[c] = Vec3::new(d[0], d[1], d[2]);
}
out
}
#[allow(clippy::needless_range_loop)]
pub fn free_velocity_sensitivity(&self) -> Vec<f64> {
let n = self.n;
let dim = 3 * n;
let mut out = vec![0.0; dim * dim];
for row in 0..dim {
for c in 0..n {
let base = 3 * c;
if self.regimes[c] == ContactRegime::Separating {
continue;
}
match &self.slide[c] {
None => {
let rows = if self.regimes[c] == ContactRegime::Sliding {
1
} else {
3
};
for r in 0..rows {
out[row * dim + base + r] += self.inv[row * dim + base + r];
}
}
Some(st) => {
out[row * dim + base] += self.inv[row * dim + base];
for i in 0..2 {
for j in 0..2 {
out[row * dim + base + 1 + j] +=
self.inv[row * dim + base + 1 + i] * st.map[i][j];
}
}
}
}
}
}
out
}
}
fn flat_impulses(impulses: &[Vec3]) -> Vec<f64> {
let mut out = Vec::with_capacity(3 * impulses.len());
for f in impulses {
out.push(f.x);
out.push(f.y);
out.push(f.z);
}
out
}
#[allow(clippy::needless_range_loop)]
pub fn impulse_sensitivity(
problem: &ContactProblem,
solution: &ContactSolution,
config: &ContactSolverConfig,
) -> Option<Vec<f64>> {
if !solution.converged {
return None;
}
if problem.n == 0 {
return Some(Vec::new());
}
Some(FixedPointSensitivity::at(problem, solution, config)?.free_velocity_sensitivity())
}
#[allow(clippy::needless_range_loop)]
pub(crate) fn kkt_matrix(
problem: &ContactProblem,
config: &ContactSolverConfig,
regimes: &[ContactRegime],
impulses: &[Vec3],
) -> Vec<f64> {
let n = problem.n;
let dim = 3 * n;
let mut k = vec![0.0; dim * dim];
for c in 0..n {
let base = 3 * c;
match regimes[c] {
ContactRegime::Separating => {
for r in 0..3 {
k[(base + r) * dim + base + r] = 1.0;
}
}
ContactRegime::Sticking => {
let reg = crate::convex::regularization_diag(problem, c, config);
for r in 0..3 {
for col in 0..dim {
k[(base + r) * dim + col] = problem.delassus[(base + r) * dim + col];
}
k[(base + r) * dim + base + r] += reg[r];
}
}
ContactRegime::Sliding => {
let reg = crate::convex::regularization_diag(problem, c, config);
for col in 0..dim {
k[base * dim + col] = problem.delassus[base * dim + col];
}
k[base * dim + base] += reg[0];
let f = impulses[c];
let ft = (f.y * f.y + f.z * f.z).sqrt();
let (tu, tw) = if ft > 1e-14 {
(f.y / ft, f.z / ft)
} else {
(0.0, 0.0)
};
let mu = problem.rows[c].mu;
k[(base + 1) * dim + base + 1] = 1.0;
k[(base + 1) * dim + base] = -mu * tu;
k[(base + 2) * dim + base + 2] = 1.0;
k[(base + 2) * dim + base] = -mu * tw;
}
}
}
k
}
#[allow(clippy::needless_range_loop)]
pub fn friction_sensitivity(
problem: &ContactProblem,
solution: &ContactSolution,
config: &ContactSolverConfig,
) -> Option<Vec<f64>> {
if !solution.converged {
return None;
}
let n = problem.n;
let dim = 3 * n;
if n == 0 {
return Some(Vec::new());
}
let regimes = classify(problem, solution, 1e-7);
let sens = impulse_sensitivity(problem, solution, config)?;
let mut out = vec![0.0; dim * n];
for c in 0..n {
if regimes[c] != ContactRegime::Sliding {
continue;
}
let base = 3 * c;
let f = solution.impulses[c];
let ft = (f.y * f.y + f.z * f.z).sqrt();
let (tu, tw) = if ft > 1e-14 {
(f.y / ft, f.z / ft)
} else {
continue;
};
let direct = [0.0, f.x * tu, f.x * tw];
let mut db = vec![0.0; dim];
for r in 0..3 {
for k in 0..dim {
db[k] += problem.delassus[k * dim + base + r] * direct[r];
}
}
for row in 0..dim {
let mut acc = 0.0;
for k in 0..dim {
acc += sens[row * dim + k] * db[k];
}
out[row * n + c] = acc
+ if row >= base && row < base + 3 {
direct[row - base]
} else {
0.0
};
}
}
Some(out)
}
pub fn depth_sensitivity(
problem: &ContactProblem,
solution: &ContactSolution,
config: &ContactSolverConfig,
solref: &crate::material::SolRef,
dt: f64,
) -> Option<Vec<f64>> {
let n = problem.n;
let dim = 3 * n;
let sens = impulse_sensitivity(problem, solution, config)?;
if n == 0 {
return Some(Vec::new());
}
let erp = solref.error_reduction(dt);
let mut out = vec![0.0; dim * n];
for c in 0..n {
let base = 3 * c;
let contact_row = &problem.rows[c];
let f = solution.impulses[c];
let dbias = if contact_row.depth > 0.0 && dt > 0.0 {
(1.0 - contact_row.impact) * erp / dt
* (contact_row.impedance + contact_row.dimpedance_ddepth * contact_row.depth)
} else {
0.0
};
let dreg = crate::convex::regularization_depth_derivative(problem, c, config);
let f_components = [f.x, f.y, f.z];
let mut dresidual = [0.0; 3];
for r in 0..3 {
dresidual[r] = f_components[r] * dreg[r];
}
dresidual[0] -= dbias;
for row in 0..dim {
let mut acc = 0.0;
for (r, dres) in dresidual.iter().enumerate() {
acc += sens[row * dim + base + r] * dres;
}
out[row * n + c] = acc;
}
}
Some(out)
}
pub fn pullback_to_free_velocity(sensitivity: &[f64], dim: usize, cotangent: &[Vec3]) -> Vec<f64> {
let mut flat = vec![0.0; dim];
for (c, g) in cotangent.iter().enumerate() {
flat[3 * c] = g.x;
flat[3 * c + 1] = g.y;
flat[3 * c + 2] = g.z;
}
let mut out = vec![0.0; dim];
for col in 0..dim {
let mut acc = 0.0;
for row in 0..dim {
acc += sensitivity[row * dim + col] * flat[row];
}
out[col] = acc;
}
out
}
pub(crate) fn solve_dense(a: &mut [f64], b: &mut [f64], dim: usize, ncols: usize) -> Option<()> {
for col in 0..dim {
let mut pivot = col;
for r in col + 1..dim {
if a[r * dim + col].abs() > a[pivot * dim + col].abs() {
pivot = r;
}
}
if a[pivot * dim + col].abs() < 1e-14 {
return None;
}
if pivot != col {
for k in 0..dim {
a.swap(col * dim + k, pivot * dim + k);
}
for k in 0..ncols {
b.swap(col * ncols + k, pivot * ncols + k);
}
}
let d = a[col * dim + col];
for k in 0..dim {
a[col * dim + k] /= d;
}
for k in 0..ncols {
b[col * ncols + k] /= d;
}
for r in 0..dim {
if r == col {
continue;
}
let factor = a[r * dim + col];
if factor == 0.0 {
continue;
}
for k in 0..dim {
a[r * dim + k] -= factor * a[col * dim + k];
}
for k in 0..ncols {
b[r * ncols + k] -= factor * b[col * ncols + k];
}
}
}
Some(())
}