use crate::domains::matrix::Matrix;
use crate::prelude::*;
fn invalid(operation: &'static str, reason: String) -> SymplexError {
SymplexError::invalid_argument(operation, reason)
}
struct StandIns {
pairs: Vec<(Ex, Ex)>,
}
impl StandIns {
fn new(vars: &[&Ex], exprs: &[&Ex]) -> Self {
let mut pairs: Vec<(Ex, Ex)> = Vec::new();
let mut k = 0usize;
for v in vars {
if v.expr_type() == crate::api::expr::ExprType::Symbol
|| pairs.iter().any(|(o, _)| o == *v)
{
continue;
}
let ctx = v.context();
let fresh = loop {
let name = if k == 0 {
"__gc".to_string()
} else {
format!("__gc_{k}")
};
k += 1;
let sym = ctx.symbol(&name);
if !vars.iter().chain(exprs).any(|e| e.contains(&sym)) {
break sym;
}
};
pairs.push(((*v).clone(), fresh));
}
StandIns { pairs }
}
fn hide(&self, e: &Ex) -> Ex {
if self.pairs.is_empty() {
return e.clone();
}
let map: Vec<(&Ex, &Ex)> = self.pairs.iter().map(|(o, s)| (o, s)).collect();
e.subs_map(&map)
}
fn show(&self, e: &Ex) -> Ex {
if self.pairs.is_empty() {
return e.clone();
}
let map: Vec<(&Ex, &Ex)> = self.pairs.iter().map(|(o, s)| (s, o)).collect();
e.subs_map(&map)
}
fn var(&self, v: &Ex) -> Ex {
self.pairs
.iter()
.find(|(o, _)| o == v)
.map_or_else(|| v.clone(), |(_, s)| s.clone())
}
fn vars(&self, vs: &[&Ex]) -> Vec<Ex> {
vs.iter().map(|v| self.var(v)).collect()
}
}
fn coord_vars<'a>(coords: &[GeneralizedCoordinate<'a>]) -> Vec<&'a Ex> {
coords
.iter()
.flat_map(|c| [c.q, c.q_dot, c.q_ddot])
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct GeneralizedCoordinate<'a> {
pub q: &'a Ex,
pub q_dot: &'a Ex,
pub q_ddot: &'a Ex,
}
pub fn total_time_derivative(expr: &Ex, coords: &[GeneralizedCoordinate<'_>]) -> Ex {
let st = StandIns::new(&coord_vars(coords), &[expr]);
if st.pairs.is_empty() {
return total_time_derivative_symbols(expr, coords);
}
let hidden: Vec<[Ex; 3]> = coords
.iter()
.map(|c| [st.var(c.q), st.var(c.q_dot), st.var(c.q_ddot)])
.collect();
let hidden_coords: Vec<GeneralizedCoordinate<'_>> = hidden
.iter()
.map(|[q, q_dot, q_ddot]| GeneralizedCoordinate { q, q_dot, q_ddot })
.collect();
st.show(&total_time_derivative_symbols(
&st.hide(expr),
&hidden_coords,
))
}
fn total_time_derivative_symbols(expr: &Ex, coords: &[GeneralizedCoordinate<'_>]) -> Ex {
let mut result = expr.context().int(0);
for coord in coords {
let df_dqi = expr.diff(coord.q);
result = &result + &(&df_dqi * coord.q_dot);
let df_dqi_dot = expr.diff(coord.q_dot);
result = &result + &(&df_dqi_dot * coord.q_ddot);
}
result
}
pub fn euler_lagrange(
kinetic_energy: &Ex,
potential_energy: &Ex,
coords: &[GeneralizedCoordinate<'_>],
) -> Vec<Ex> {
let st = StandIns::new(&coord_vars(coords), &[kinetic_energy, potential_energy]);
if st.pairs.is_empty() {
return euler_lagrange_symbols(kinetic_energy, potential_energy, coords);
}
let hidden: Vec<[Ex; 3]> = coords
.iter()
.map(|c| [st.var(c.q), st.var(c.q_dot), st.var(c.q_ddot)])
.collect();
let hidden_coords: Vec<GeneralizedCoordinate<'_>> = hidden
.iter()
.map(|[q, q_dot, q_ddot]| GeneralizedCoordinate { q, q_dot, q_ddot })
.collect();
euler_lagrange_symbols(
&st.hide(kinetic_energy),
&st.hide(potential_energy),
&hidden_coords,
)
.iter()
.map(|e| st.show(e))
.collect()
}
fn euler_lagrange_symbols(
kinetic_energy: &Ex,
potential_energy: &Ex,
coords: &[GeneralizedCoordinate<'_>],
) -> Vec<Ex> {
let lagrangian = kinetic_energy - potential_energy;
let mut equations = Vec::with_capacity(coords.len());
for coord in coords {
let dl_dqi_dot = lagrangian.diff(coord.q_dot);
let dt_dl_dqi_dot = total_time_derivative_symbols(&dl_dqi_dot, coords);
let dl_dqi = lagrangian.diff(coord.q);
let eq_i = &dt_dl_dqi_dot - &dl_dqi;
equations.push(eq_i.eval());
}
equations
}
pub fn mass_matrix(kinetic_energy: &Ex, qdot_vars: &[&Ex]) -> Result<Matrix, SymplexError> {
if qdot_vars.is_empty() {
return Err(invalid(
"dynamics::mass_matrix",
"need at least one velocity variable".into(),
));
}
let st = StandIns::new(qdot_vars, &[kinetic_energy]);
let t = st.hide(kinetic_energy);
let qd = st.vars(qdot_vars);
let n = qdot_vars.len();
let mut rows = Vec::with_capacity(n);
for i in 0..n {
let mut row = Vec::with_capacity(n);
for j in 0..n {
let m_ij = st.show(&t.diff(&qd[i]).diff(&qd[j]));
row.push(m_ij);
}
rows.push(row);
}
Ok(Matrix::from_rows_unchecked(rows).eval())
}
pub fn christoffel_symbols(
mass_mat: &Matrix,
q_vars: &[&Ex],
) -> Result<Vec<Vec<Vec<Ex>>>, SymplexError> {
let n = q_vars.len();
if n == 0 || mass_mat.shape() != (n, n) {
return Err(invalid(
"dynamics::christoffel_symbols",
format!(
"mass matrix shape {:?} does not match {n} coordinates",
mass_mat.shape()
),
));
}
let half = q_vars[0].context().rational(1, 2);
let entries: Vec<&Ex> = mass_mat.iter().collect();
let st = StandIns::new(q_vars, &entries);
let q = st.vars(q_vars);
let m = Matrix::from_fn_unchecked(n, n, |i, j| st.hide(mass_mat.get(i, j)));
let mut result = Vec::with_capacity(n);
for i in 0..n {
let mut plane = Vec::with_capacity(n);
for j in 0..n {
let mut row = Vec::with_capacity(n);
for k in 0..n {
let dm_ij_dqk = m.get(i, j).diff(&q[k]);
let dm_ik_dqj = m.get(i, k).diff(&q[j]);
let dm_jk_dqi = m.get(j, k).diff(&q[i]);
let gamma = &half * &(&(&dm_ij_dqk + &dm_ik_dqj) - &dm_jk_dqi);
row.push(st.show(&gamma));
}
plane.push(row);
}
result.push(plane);
}
Ok(result)
}
pub fn coriolis_matrix(
mass_mat: &Matrix,
q_vars: &[&Ex],
qdot_vars: &[&Ex],
) -> Result<Matrix, SymplexError> {
let n = q_vars.len();
if qdot_vars.len() != n {
return Err(invalid(
"dynamics::coriolis_matrix",
format!(
"q_vars and qdot_vars must have the same length, got {n} and {}",
qdot_vars.len()
),
));
}
let christoffel = christoffel_symbols(mass_mat, q_vars)?;
let mut rows = Vec::with_capacity(n);
for christoffel_i in christoffel.iter().take(n) {
let mut row = Vec::with_capacity(n);
for christoffel_ij in christoffel_i.iter().take(n) {
let mut c_ij = qdot_vars[0].context().int(0);
for k in 0..n {
c_ij = &c_ij + &(&christoffel_ij[k] * qdot_vars[k]);
}
row.push(c_ij);
}
rows.push(row);
}
Ok(Matrix::from_rows_unchecked(rows).eval())
}
pub fn gravity_vector(potential_energy: &Ex, q_vars: &[&Ex]) -> Vec<Ex> {
let st = StandIns::new(q_vars, &[potential_energy]);
let v = st.hide(potential_energy);
st.vars(q_vars)
.iter()
.map(|qi| st.show(&v.diff(qi)).eval())
.collect()
}
#[derive(Clone, Debug)]
pub struct ManipulatorEquation {
pub mass: Matrix,
pub coriolis: Matrix,
pub gravity: Vec<Ex>,
}
pub fn manipulator_equation(
kinetic_energy: &Ex,
potential_energy: &Ex,
q_vars: &[&Ex],
qdot_vars: &[&Ex],
) -> Result<ManipulatorEquation, SymplexError> {
const OP: &str = "dynamics::manipulator_equation";
if q_vars.len() != qdot_vars.len() {
return Err(invalid(
OP,
format!(
"q_vars and qdot_vars must have the same length, got {} and {}",
q_vars.len(),
qdot_vars.len()
),
));
}
let all: Vec<&Ex> = q_vars.iter().chain(qdot_vars).copied().collect();
let st = StandIns::new(&all, &[kinetic_energy, potential_energy]);
if st.pairs.is_empty() {
return manipulator_equation_symbols(kinetic_energy, potential_energy, q_vars, qdot_vars);
}
let q = st.vars(q_vars);
let qd = st.vars(qdot_vars);
let qr: Vec<&Ex> = q.iter().collect();
let qdr: Vec<&Ex> = qd.iter().collect();
let me = manipulator_equation_symbols(
&st.hide(kinetic_energy),
&st.hide(potential_energy),
&qr,
&qdr,
)?;
Ok(ManipulatorEquation {
mass: me.mass.map(|e| st.show(e)),
coriolis: me.coriolis.map(|e| st.show(e)),
gravity: me.gravity.iter().map(|e| st.show(e)).collect(),
})
}
fn manipulator_equation_symbols(
kinetic_energy: &Ex,
potential_energy: &Ex,
q_vars: &[&Ex],
qdot_vars: &[&Ex],
) -> Result<ManipulatorEquation, SymplexError> {
const OP: &str = "dynamics::manipulator_equation";
let m = mass_matrix(kinetic_energy, qdot_vars)?;
let n = qdot_vars.len();
for i in 0..n {
for j in 0..n {
let entry = m.get(i, j);
if let Some(v) = qdot_vars.iter().find(|v| entry.free_symbols().contains(v)) {
return Err(invalid(
OP,
format!(
"the kinetic energy is not of degree at most two in the velocities: \
∂²T/∂{}∂{} = {entry} depends on {v}",
qdot_vars[i], qdot_vars[j]
),
));
}
}
}
let c = coriolis_matrix(&m, q_vars, qdot_vars)?;
let ctx = kinetic_energy.context();
let zero = ctx.int(0);
let at_rest: Vec<(&Ex, &Ex)> = qdot_vars.iter().map(|v| (*v, &zero)).collect();
let b: Vec<Ex> = qdot_vars
.iter()
.map(|v| kinetic_energy.diff(v).subs_map(&at_rest).eval())
.collect();
let t0 = kinetic_energy.subs_map(&at_rest).eval();
let c = Matrix::from_fn_unchecked(n, n, |i, j| {
let gyro = &b[i].diff(q_vars[j]) - &b[j].diff(q_vars[i]);
(c.get(i, j) + &gyro).eval()
});
let g = q_vars
.iter()
.map(|qi| (&potential_energy.diff(qi) - &t0.diff(qi)).eval())
.collect();
Ok(ManipulatorEquation {
mass: m.eval(),
coriolis: c,
gravity: g,
})
}