use crate::dataset::DataSet;
use crate::error::{PhopError, Result};
use scirs2_core::ndarray::Array2;
pub type Dimension = [i32; 7];
pub const DIMENSIONLESS: Dimension = [0; 7];
pub const TIME: Dimension = [1, 0, 0, 0, 0, 0, 0];
pub const LENGTH: Dimension = [0, 1, 0, 0, 0, 0, 0];
pub const MASS: Dimension = [0, 0, 1, 0, 0, 0, 0];
#[derive(Clone, Copy)]
struct Frac {
num: i64,
den: i64,
}
fn gcd(a: i64, b: i64) -> i64 {
let (mut a, mut b) = (a.abs(), b.abs());
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a.max(1)
}
impl Frac {
fn new(num: i64, den: i64) -> Frac {
debug_assert!(den != 0);
let s = if den < 0 { -1 } else { 1 };
let (num, den) = (num * s, den * s);
let g = gcd(num, den);
Frac {
num: num / g,
den: den / g,
}
}
fn int(n: i64) -> Frac {
Frac { num: n, den: 1 }
}
fn zero() -> Frac {
Frac { num: 0, den: 1 }
}
fn is_zero(self) -> bool {
self.num == 0
}
fn sub(self, o: Frac) -> Frac {
Frac::new(self.num * o.den - o.num * self.den, self.den * o.den)
}
fn mul(self, o: Frac) -> Frac {
Frac::new(self.num * o.num, self.den * o.den)
}
fn div(self, o: Frac) -> Frac {
Frac::new(self.num * o.den, self.den * o.num)
}
}
#[must_use]
pub fn pi_groups(dims: &[Dimension]) -> Vec<Vec<i32>> {
let n = dims.len();
if n == 0 {
return Vec::new();
}
let mut m: Vec<Vec<Frac>> = (0..7)
.map(|r| (0..n).map(|c| Frac::int(i64::from(dims[c][r]))).collect())
.collect();
let rows = 7;
let mut pivot_col = vec![usize::MAX; rows];
let mut r = 0usize;
for c in 0..n {
if r >= rows {
break;
}
let Some(p) = (r..rows).find(|&i| !m[i][c].is_zero()) else {
continue;
};
m.swap(r, p);
let pivot = m[r][c];
for cell in m[r].iter_mut() {
*cell = cell.div(pivot);
}
let prow = m[r].clone();
for (i, mrow) in m.iter_mut().enumerate() {
if i != r && !mrow[c].is_zero() {
let factor = mrow[c];
for (cell, &pj) in mrow.iter_mut().zip(prow.iter()) {
*cell = cell.sub(factor.mul(pj));
}
}
}
pivot_col[r] = c;
r += 1;
}
let pivots: Vec<usize> = pivot_col
.iter()
.copied()
.filter(|&c| c != usize::MAX)
.collect();
let is_pivot = |c: usize| pivots.contains(&c);
let mut basis = Vec::new();
for free in (0..n).filter(|&c| !is_pivot(c)) {
let mut vec_q = vec![Frac::zero(); n];
vec_q[free] = Frac::int(1);
for (row, &pc) in pivot_col.iter().enumerate() {
if pc != usize::MAX {
vec_q[pc] = Frac::zero().sub(m[row][free]);
}
}
let lcm = vec_q
.iter()
.fold(1i64, |acc, f| acc / gcd(acc, f.den) * f.den);
let mut ints: Vec<i32> = vec_q
.iter()
.map(|f| (f.num * (lcm / f.den)) as i32)
.collect();
let g = ints
.iter()
.fold(0i64, |acc, &v| gcd(acc, i64::from(v)))
.max(1) as i32;
for v in &mut ints {
*v /= g;
}
if let Some(&first) = ints.iter().find(|&&v| v != 0) {
if first < 0 {
for v in &mut ints {
*v = -*v;
}
}
}
basis.push(ints);
}
basis
}
impl DataSet {
pub fn to_dimensionless(&self, feature_dims: &[Dimension]) -> Result<(DataSet, Vec<Vec<i32>>)> {
let nv = self.n_vars();
if feature_dims.len() != nv {
return Err(PhopError::ShapeMismatch(format!(
"expected {nv} feature dimensions, got {}",
feature_dims.len()
)));
}
let groups = pi_groups(feature_dims);
if groups.is_empty() {
return Err(PhopError::NotConverged(
"no dimensionless π-groups: the features are dimensionally independent".to_string(),
));
}
let n_rows = self.len();
let n_pi = groups.len();
let mut data = Vec::with_capacity(n_rows * n_pi);
for row in 0..n_rows {
for g in &groups {
let mut v = 1.0_f64;
for (j, &e) in g.iter().enumerate() {
if e != 0 {
v *= self.x[[row, j]].powi(e);
}
}
data.push(v);
}
}
let x = Array2::from_shape_vec((n_rows, n_pi), data)
.map_err(|e| PhopError::ShapeMismatch(e.to_string()))?;
let feature_names = (0..n_pi).map(|k| format!("pi{k}")).collect();
let ds = DataSet {
x,
y: self.y.clone(),
feature_names,
target_name: self.target_name.clone(),
};
Ok((ds, groups))
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn pi_group_for_velocity_time_length() {
let dims = [
LENGTH, TIME, [-1, 1, 0, 0, 0, 0, 0], ];
let groups = pi_groups(&dims);
assert_eq!(
groups.len(),
1,
"expected exactly one π-group, got {groups:?}"
);
let g = &groups[0];
let nonzero: Vec<i32> = g.clone();
assert!(
nonzero == vec![-1, 1, 1] || nonzero == vec![1, -1, -1],
"unexpected π-group {nonzero:?}"
);
}
#[test]
fn to_dimensionless_yields_constant_group() {
let n = 12;
let mut xd = Vec::new();
for i in 0..n {
let x0 = 1.0 + i as f64; let x1 = 2.0 + i as f64; let x2 = x0 / x1; xd.extend_from_slice(&[x0, x1, x2]);
}
let x = Array2::from_shape_vec((n, 3), xd).unwrap();
let y = Array1::from(vec![0.0; n]);
let ds = DataSet::from_arrays(x, y).unwrap();
let dims = [LENGTH, TIME, [-1, 1, 0, 0, 0, 0, 0]];
let (red, groups) = ds.to_dimensionless(&dims).unwrap();
assert_eq!(groups.len(), 1);
assert_eq!(red.n_vars(), 1);
for r in 0..n {
let v = red.x[[r, 0]];
assert!((v - 1.0).abs() < 1e-9, "π not constant: {v}");
}
}
#[test]
fn independent_dims_have_no_group() {
assert!(pi_groups(&[LENGTH, TIME]).is_empty());
}
}