From 27f88455fba7004b76759b21b02cad516e8d28af Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 8 Nov 2024 19:48:26 -0800 Subject: [PATCH 1/4] Turn assertionless tests into Cargo examples --- app-proto/examples/point-on-sphere.rs | 38 ++++++++++++ app-proto/examples/three-spheres.rs | 40 ++++++++++++ app-proto/run-examples | 10 +-- app-proto/src/engine.rs | 89 +-------------------------- app-proto/src/lib.rs | 1 + 5 files changed, 85 insertions(+), 93 deletions(-) create mode 100644 app-proto/examples/point-on-sphere.rs create mode 100644 app-proto/examples/three-spheres.rs create mode 100644 app-proto/src/lib.rs diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs new file mode 100644 index 0000000..0e4765a --- /dev/null +++ b/app-proto/examples/point-on-sphere.rs @@ -0,0 +1,38 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs new file mode 100644 index 0000000..d348b18 --- /dev/null +++ b/app-proto/examples/three-spheres.rs @@ -0,0 +1,40 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..3 { + for k in j..3 { + gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + gram_to_be + }; + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, 1.0), + sphere(-0.5, a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) + ]) + }; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 6a5e3ae..4d35882 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -1,8 +1,8 @@ -# based on "Enabling print statements in Cargo tests", by Jon Almeida +# run all Cargo examples, as described here: # -# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html # -cargo test -- --nocapture engine::tests::irisawa_hexlet_test -cargo test -- --nocapture engine::tests::three_spheres_example -cargo test -- --nocapture engine::tests::point_on_sphere_example +cargo run --example three-spheres +cargo run --example point-on-sphere \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 2978a9a..c5f0a8e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -112,7 +112,7 @@ impl DescentHistory { // the Lorentz form lazy_static! { - static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + pub static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, @@ -412,91 +412,4 @@ mod tests { println!("{:<4} │ {}", step, scaled_loss); } } - - // --- process inspection examples --- - - // these tests are meant for human inspection, not automated use. run them - // one at a time in `--nocapture` mode and read through the results and - // optimization histories that they print out. the `run-examples` script - // will run all of them - - #[test] - fn three_spheres_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } - } - entries - }); - let guess = { - let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ - sphere(1.0, 0.0, 0.0, 1.0), - sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0) - ]) - }; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } - - #[test] - fn point_on_sphere_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..2 { - for k in 0..2 { - entries.push(MatrixEntry { - index: (j, k), - value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } - }); - } - } - entries - }); - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0)]; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } } \ No newline at end of file diff --git a/app-proto/src/lib.rs b/app-proto/src/lib.rs new file mode 100644 index 0000000..0d9bc4a --- /dev/null +++ b/app-proto/src/lib.rs @@ -0,0 +1 @@ +pub mod engine; \ No newline at end of file From 3a0f3a8d1c4db74b950324787bf2d1e25a7711c9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 9 Nov 2024 00:19:09 -0800 Subject: [PATCH 2/4] Streamline Gram matrix setup for Irisawa hexlet --- app-proto/src/engine.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5f0a8e..7e6e5ba 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -337,36 +337,33 @@ mod tests { // #[test] fn irisawa_hexlet_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); + let gram = { + let mut gram_to_be = PartialMatrix::new(); for s in 0..9 { // each sphere is represented by a spacelike vector - entries.push(MatrixEntry { index: (s, s), value: 1.0 }); + gram_to_be.push_sym(s, s, 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 }); + gram_to_be.push_sym(0, s, 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 }); + gram_to_be.push_sym(s, n, -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 }); + gram_to_be.push_sym(s, s_next, -1.0); } } - entries - }); + gram_to_be + }; let guess = DMatrix::from_columns( [ sphere(0.0, 0.0, 0.0, 15.0), From 4094301318931759a47564fdc7691c3fa9df9fa1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 9 Nov 2024 01:04:44 -0800 Subject: [PATCH 3/4] Factor out the realization of the Irisawa hexlet --- app-proto/src/engine.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 7e6e5ba..3d4ef8c 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -335,8 +335,7 @@ mod tests { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - #[test] - fn irisawa_hexlet_test() { + fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); for s in 0..9 { @@ -364,6 +363,7 @@ mod tests { } gram_to_be }; + let guess = DMatrix::from_columns( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -378,17 +378,31 @@ mod tests { ) ).collect::>().as_slice() ); + + // the frozen entries fix the radii of the circumscribing sphere, the + // "sun" and "moon" spheres, and one of the chain spheres let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); - const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_gram( + + realize_gram( &gram, guess, &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } + + #[test] + fn irisawa_hexlet_test() { + // solve Irisawa's problem + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + + // check against Irisawa's solution 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!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } + + // print info print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { println!("Target accuracy achieved!"); From a06d5942e38b43bac059423dd7876e3dee45cde7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 9 Nov 2024 11:22:15 -0800 Subject: [PATCH 4/4] Separate test and example for Irisawa hexlet Put shared code in the conditionally compiled `engine::irisawa` module. --- app-proto/Cargo.toml | 6 ++ app-proto/examples/irisawa-hexlet.rs | 25 +++++ app-proto/run-examples | 1 + app-proto/src/engine.rs | 140 ++++++++++++--------------- 4 files changed, 94 insertions(+), 78 deletions(-) create mode 100644 app-proto/examples/irisawa-hexlet.rs diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e623b26..3814c38 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] default = ["console_error_panic_hook"] +irisawa = [] [dependencies] itertools = "0.13.0" @@ -36,7 +37,12 @@ features = [ 'WebGlVertexArrayObject' ] +# the self-dependency specifies features to use for tests and examples +# +# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987 +# [dev-dependencies] +dyna3 = { path = ".", default-features = false, features = ["irisawa"] } wasm-bindgen-test = "0.3.34" [profile.release] diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs new file mode 100644 index 0000000..fc14f91 --- /dev/null +++ b/app-proto/examples/irisawa-hexlet.rs @@ -0,0 +1,25 @@ +use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; + +fn main() { + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); + } + } + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 4d35882..9791a9d 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -4,5 +4,6 @@ # http://xion.io/post/code/rust-examples.html # +cargo run --example irisawa-hexlet cargo run --example three-spheres cargo run --example point-on-sphere \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 3d4ef8c..e33a261 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -276,66 +276,21 @@ pub fn realize_gram( // --- tests --- -#[cfg(test)] -mod tests { +// 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_) +// +// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki +// https://www.nippon.com/en/japan-topics/c12801/ +// +#[cfg(feature = "irisawa")] +pub mod irisawa { use std::{array, f64::consts::PI}; use super::*; - #[test] - fn sub_proj_test() { - let target = PartialMatrix(vec![ - MatrixEntry { index: (0, 0), value: 19.0 }, - MatrixEntry { index: (0, 2), value: 39.0 }, - MatrixEntry { index: (1, 1), value: 59.0 }, - MatrixEntry { index: (1, 2), value: 69.0 } - ]); - let attempt = DMatrix::::from_row_slice(2, 3, &[ - 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0 - ]); - let expected_result = DMatrix::::from_row_slice(2, 3, &[ - 18.0, 0.0, 36.0, - 0.0, 54.0, 63.0 - ]); - assert_eq!(target.sub_proj(&attempt), expected_result); - } - - #[test] - fn zero_loss_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } - } - entries - }); - let config = { - let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ - sphere(1.0, 0.0, 0.0, a), - sphere(-0.5, a, 0.0, a), - sphere(-0.5, -a, 0.0, a) - ]) - }; - let state = SearchState::from_config(&gram, config); - 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_) - // - // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki - // https://www.nippon.com/en/japan-topics/c12801/ - // - fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); for s in 0..9 { @@ -388,12 +343,62 @@ mod tests { scaled_tol, 0.5, 0.9, 1.1, 200, 110 ) } +} + +#[cfg(test)] +mod tests { + use super::{*, irisawa::realize_irisawa_hexlet}; + + #[test] + fn sub_proj_test() { + let target = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 19.0 }, + MatrixEntry { index: (0, 2), value: 39.0 }, + MatrixEntry { index: (1, 1), value: 59.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } + ]); + let attempt = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 18.0, 0.0, 36.0, + 0.0, 54.0, 63.0 + ]); + assert_eq!(target.sub_proj(&attempt), expected_result); + } + + #[test] + fn zero_loss_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let config = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, a), + sphere(-0.5, a, 0.0, a), + sphere(-0.5, -a, 0.0, a) + ]) + }; + let state = SearchState::from_config(&gram, config); + assert!(state.loss.abs() < f64::EPSILON); + } #[test] fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL); // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -401,26 +406,5 @@ mod tests { for (k, diam) in solution_diams.into_iter().enumerate() { assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } - - // print info - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - println!("\nChain diameters:"); - println!(" {} sun (given)", 1.0 / config[(3, 3)]); - for k in 4..9 { - println!(" {} sun", 1.0 / config[(3, k)]); - } - } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } } } \ No newline at end of file