Integrate engine into application prototype #15

Merged
glen merged 24 commits from engine-integration into main 2024-11-12 00:46:16 +00:00
2 changed files with 105 additions and 14 deletions
Showing only changes of commit 9f8632efb3 - Show all commits

View File

@ -3,5 +3,6 @@
# https://jonalmeida.com/posts/2015/01/23/print-cargo/ # https://jonalmeida.com/posts/2015/01/23/print-cargo/
# #
cargo test -- --nocapture engine::tests::irisawa_hexlet_test
cargo test -- --nocapture engine::tests::three_spheres_example cargo test -- --nocapture engine::tests::three_spheres_example
cargo test -- --nocapture engine::tests::point_on_sphere_example cargo test -- --nocapture engine::tests::point_on_sphere_example

View File

@ -37,7 +37,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
struct MatrixEntry { struct MatrixEntry {
index: (usize, usize), index: (usize, usize),
val: f64 value: f64
} }
struct PartialMatrix(Vec<MatrixEntry>); struct PartialMatrix(Vec<MatrixEntry>);
@ -56,7 +56,7 @@ impl PartialMatrix {
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols()); let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
let PartialMatrix(entries) = self; let PartialMatrix(entries) = self;
for ent in entries { for ent in entries {
result[ent.index] = ent.val - rhs[ent.index]; result[ent.index] = ent.value - rhs[ent.index];
} }
result result
} }
@ -141,7 +141,7 @@ fn realize_gram(
let total_dim = element_dim * assembly_dim; let total_dim = element_dim * assembly_dim;
// scale the tolerance // scale the tolerance
let scale_adjustment = ((guess.ncols() - frozen.len()) as f64).sqrt(); let scale_adjustment = (gram.0.len() as f64).sqrt();
let tol = scale_adjustment * scaled_tol; let tol = scale_adjustment * scaled_tol;
// convert the frozen indices to stacked format // convert the frozen indices to stacked format
@ -153,8 +153,8 @@ fn realize_gram(
let mut state = SearchState::from_config(gram, guess); let mut state = SearchState::from_config(gram, guess);
for _ in 0..max_descent_steps { for _ in 0..max_descent_steps {
// stop if the loss is tolerably low // stop if the loss is tolerably low
println!("loss: {}", state.loss); println!("scaled loss: {}", state.loss / scale_adjustment);
/*println!("projected error: {}", state.err_proj);*/ /* println!("projected error: {}", state.err_proj); */
if state.loss < tol { break; } if state.loss < tol { break; }
// find the negative gradient of the loss function // find the negative gradient of the loss function
@ -182,6 +182,7 @@ fn realize_gram(
// regularize the Hessian // regularize the Hessian
let min_eigval = hess.symmetric_eigenvalues().min(); let min_eigval = hess.symmetric_eigenvalues().min();
/* println!("lowest eigenvalue: {}", min_eigval); */
if min_eigval <= 0.0 { if min_eigval <= 0.0 {
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
} }
@ -198,6 +199,12 @@ fn realize_gram(
} }
// compute the Newton step // compute the Newton step
/*
we need to either handle or eliminate the case where the minimum
eigenvalue of the Hessian is zero, so the regularized Hessian is
singular. right now, this causes the Cholesky decomposition to return
`None`, leading to a panic when we unrap
*/
let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked);
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
@ -217,17 +224,17 @@ fn realize_gram(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::f64; use std::{array, f64::consts::PI};
use super::*; use super::*;
#[test] #[test]
fn sub_proj_test() { fn sub_proj_test() {
let target = PartialMatrix(vec![ let target = PartialMatrix(vec![
MatrixEntry { index: (0, 0), val: 19.0 }, MatrixEntry { index: (0, 0), value: 19.0 },
MatrixEntry { index: (0, 2), val: 39.0 }, MatrixEntry { index: (0, 2), value: 39.0 },
MatrixEntry { index: (1, 1), val: 59.0 }, MatrixEntry { index: (1, 1), value: 59.0 },
MatrixEntry { index: (1, 2), val: 69.0 } MatrixEntry { index: (1, 2), value: 69.0 }
]); ]);
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[ let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
@ -248,7 +255,7 @@ mod tests {
for k in 0..3 { for k in 0..3 {
entries.push(MatrixEntry { entries.push(MatrixEntry {
index: (j, k), index: (j, k),
val: if j == k { 1.0 } else { -1.0 } value: if j == k { 1.0 } else { -1.0 }
}); });
} }
} }
@ -266,6 +273,88 @@ mod tests {
assert!(state.loss.abs() < f64::EPSILON); assert!(state.loss.abs() < f64::EPSILON);
} }
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
// below includes a nice translation of the problem statement, which was
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
// Present_)
//
glen marked this conversation as resolved
Review

love that there are tests right in here, we definitely want to keep that up and have them be as pervasive as possible.

love that there are tests right in here, we definitely want to keep that up and have them be as pervasive as possible.
Review

Yes, this is one of the recommended ways to organize tests in Rust!

Yes, this is one of the [recommended ways](https://doc.rust-lang.org/book/ch11-03-test-organization.html) to organize tests in Rust!
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
// https://www.nippon.com/en/japan-topics/c12801/
//
#[test]
fn irisawa_hexlet_test() {
let gram = PartialMatrix({
let mut entries = Vec::<MatrixEntry>::new();
for s in 0..9 {
// each sphere is represented by a spacelike vector
entries.push(MatrixEntry { index: (s, s), value: 1.0 });
// the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation
if s > 0 {
entries.push(MatrixEntry { index: (0, s), value: 1.0 });
entries.push(MatrixEntry { index: (s, 0), value: 1.0 });
}
if s > 2 {
// each chain sphere is tangent to the "sun" and "moon"
// spheres, with opposing orientation
for n in 1..3 {
entries.push(MatrixEntry { index: (s, n), value: -1.0 });
entries.push(MatrixEntry { index: (n, s), value: -1.0 });
}
// each chain sphere is tangent to the next chain sphere,
// with opposing orientation
let s_next = 3 + (s-2) % 6;
entries.push(MatrixEntry { index: (s, s_next), value: -1.0 });
entries.push(MatrixEntry { index: (s_next, s), value: -1.0 });
}
}
entries
});
let guess = DMatrix::from_columns(
[
sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0),
sphere(0.0, 0.0, 11.0, 3.0)
].into_iter().chain(
(1..=6).map(
|k| {
let ang = (k as f64) * PI/3.0;
sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5)
}
)
).collect::<Vec<_>>().as_slice()
);
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k));
const SCALED_TOL: f64 = 1.0e-12;
let (config, success) = realize_gram(
&gram, guess, &frozen,
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
let final_state = SearchState::from_config(&gram, config);
if success {
println!("Target accuracy achieved!");
} else {
println!("Failed to reach target accuracy");
}
println!("Loss: {}", final_state.loss);
if success {
println!("\nChain diameters:");
println!(" {} sun (given)", 1.0 / final_state.config[(3, 3)]);
for k in 4..9 {
println!(" {} sun", 1.0 / final_state.config[(3, k)]);
}
}
let entry_tol = SCALED_TOL.sqrt();
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
for (k, diam) in solution_diams.into_iter().enumerate() {
assert!((final_state.config[(3, k)] - 1.0 / diam).abs() < entry_tol);
}
}
// --- process inspection examples --- // --- process inspection examples ---
// these tests are meant for human inspection, not automated use. run them // these tests are meant for human inspection, not automated use. run them
@ -281,7 +370,7 @@ mod tests {
for k in 0..3 { for k in 0..3 {
entries.push(MatrixEntry { entries.push(MatrixEntry {
index: (j, k), index: (j, k),
val: if j == k { 1.0 } else { -1.0 } value: if j == k { 1.0 } else { -1.0 }
}); });
} }
} }
@ -318,7 +407,7 @@ mod tests {
for k in 0..2 { for k in 0..2 {
entries.push(MatrixEntry { entries.push(MatrixEntry {
index: (j, k), index: (j, k),
val: if (j, k) == (1, 1) { 1.0 } else { 0.0 } value: if (j, k) == (1, 1) { 1.0 } else { 0.0 }
}); });
} }
} }
@ -328,9 +417,10 @@ mod tests {
point(0.0, 0.0, 2.0), point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 1.0) sphere(0.0, 0.0, 0.0, 1.0)
]); ]);
let frozen = [(3, 0)];
println!(); println!();
let (config, success) = realize_gram( let (config, success) = realize_gram(
&gram, guess, &[(3, 0)], &gram, guess, &frozen,
1.0e-12, 0.5, 0.9, 1.1, 200, 110 1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);