From df31e17389d74ff24888b324fd21324c9af56fc4 Mon Sep 17 00:00:00 2001 From: tobi Date: Tue, 12 Mar 2024 16:07:37 +0100 Subject: [PATCH] Add new algorithm for checking kernel validity new algorithm checks the actual resulting kernels instead of solving the problem with some mathematical representation. This delivers the expected results but is less performant. But this is okay since the calculation is only done once at startup. --- src/kernel_test.rs | 76 +++++++-------------------------------- src/map.rs | 89 +++++++++++++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 96 deletions(-) diff --git a/src/kernel_test.rs b/src/kernel_test.rs index 71cfaab..62013a2 100644 --- a/src/kernel_test.rs +++ b/src/kernel_test.rs @@ -7,12 +7,12 @@ mod random; mod walker; use std::f64::consts::SQRT_2; - +use std::process::exit; use crate::{editor::*, fps_control::*, grid_render::*, map::*, position::*, random::*, walker::*}; use egui::emath::Numeric; -use egui::{Label}; +use egui::Label; use macroquad::color::*; use macroquad::shapes::*; use macroquad::window::clear_background; @@ -156,81 +156,31 @@ async fn main() { outer_size: 5, }; - Kernel::evaluate_kernels(19); + let (all_valid_radii_sqr, max_inner_radius_for_outer, max_outer_radius_for_inner) = + Kernel::precompute_kernel_configurations(19); loop { fps_ctrl.on_frame_start(); editor.on_frame_start(); - define_egui(&mut editor, &mut state); - editor.set_cam(&map); editor.handle_user_inputs(&map); - clear_background(GRAY); draw_walker(&walker); - walker.kernel = Kernel::new(state.outer_size, state.outer_radius_sqr); + let outer_kernel = Kernel::new(state.outer_size, state.outer_radius_sqr); + walker.kernel = outer_kernel.clone(); draw_thingy(&walker, false); - let weird_factor: f64 = 2.0 * SQRT_2 * state.outer_radius_sqr.to_f64().sqrt(); - let max_inner_radius_sqr: f64 = state.outer_radius_sqr.to_f64() - weird_factor + 2.0; - // as this is based on a less or equal equation we can round down to the next integer, - let valid_inner_radius_sqr: usize = (max_inner_radius_sqr).round().to_usize().unwrap(); - - // NOTE: it seems to work with a crappy fix like this using +0.2 ... this is the case - // because an extra distance of sqrt(2) is required in the "worst case" if the - // "most outward" blocks are exactly on the limiting radius (e.g. max radius). Then, The full - // sqrt(2) are required, in other cases less is okay. Therefore the sqrt(2) assumption - // might not be that useful? What other possible ways could there be to validate if outer - // kernel has at least "one padding" around the inner kernel? - - // TODO: idea: dont do radius+sqrt(2), but radius-unused+sqrt(2), where unused is the - // amount of the radius that is not required for active blocks. This means i need to - // somehow get the "actual limiting radius" - - // NOTE: okay jesus christ im going insane. it turns out that this entire approach is - // faulty to begin with. When only using kernel-radii that are exactly limiting the most - // outter blocks, i expected the remaining margin to be sqrt(2) (see get_unique_radii_sqr). - // But, it turns out that this is not correct. Imagine 3x3 - 4x4. Then yes. but imagine - // 2x3 - 3x4, then yes the difference vector is (1,1) which has a distance of sqrt(2). But - // if you draw the whole thing using vectors you'll see that the distance between the radii - // and the limited blocks is only both equal to sqrt(2) if the vectors for the limiting - // are overlapping. I tried to think about doing funky calculations using angles, but the - // inner circle is not known and therefore the angle is also not known. - // Instead i came up with a completly new Kernel representation, which would make this - // entire calculation obsolete. Instead of defining some circularity/radius i could define - // the 'limiting block' (x, y), which would also implicitly define a radius. I also should ensure - // that x and y are positive. Then, i could simply reduce -1 from both and have the 'one - // smaller but valid' kernel! It might also make sense to have some x>=y constraint because - // otherwise (3,2) and (2,3) would result in the same kernel. - // This idea could also have some problems that im currently not noticing :D - // (x,y) are actually the offset to the center, so the largest possible value resulting in - // a square kernel would be (size - center - 1, size - center - 1) and the smallest possible value - // would be (0, size - center - 1), which should result in fully circular kernel. - // the only downside i can think of is that, while i can calculate a circularity (0 - 1) - // based on these informations, i cant generate a kernel based on some circularity in some - // trivial way. This would be nice when changing the size of the kernel, but wanting that - // it remains a similar shape. - - if state.inner_radius_sqr as f64 > max_inner_radius_sqr { - state.inner_radius_sqr = valid_inner_radius_sqr; - } - - walker.kernel = Kernel::new(state.inner_size, state.inner_radius_sqr); - - // let valid_radii_sqr = Kernel::get_unique_radii_sqr(state.inner_size); - - // dbg!(( - // &weird_factor, - // &max_inner_radius_sqr, - // &valid_inner_radius_sqr, - // &state, - // &walker.kernel, - // &valid_radii_sqr - // )); + let inner_kernel = Kernel::new(state.inner_size, state.inner_radius_sqr); + walker.kernel = inner_kernel.clone(); draw_thingy(&walker, true); + let max_valid_inner_radius = max_inner_radius_for_outer.get(&state.outer_radius_sqr); + if state.inner_radius_sqr > *max_valid_inner_radius.unwrap() { + dbg!("invalid", &state.inner_radius_sqr, &max_valid_inner_radius); + } + egui_macroquad::draw(); fps_ctrl.wait_for_next_frame().await; } diff --git a/src/map.rs b/src/map.rs index 0c45756..870e465 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,7 +1,5 @@ use std::f64::consts::SQRT_2; - - use crate::CuteWalker; use crate::Position; use egui::emath::Numeric; @@ -29,7 +27,7 @@ pub struct Map { pub width: usize, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Kernel { pub size: usize, pub radius_sqr: usize, @@ -38,6 +36,7 @@ pub struct Kernel { impl Kernel { pub fn new(size: usize, radius_sqr: usize) -> Kernel { + assert!(size % 2 == 1, "kernel size must be odd"); let vector = Kernel::get_kernel_vector(size, radius_sqr); Kernel { size, @@ -50,10 +49,16 @@ impl Kernel { (size - 1) / 2 } + fn center(&self) -> usize { + (self.size - 1) / 2 + } + + fn max_offset(&self) -> usize { + (self.size - 1) / 2 + } + pub fn get_valid_radius_bounds(size: usize) -> (usize, usize) { - // TODO: center and min_radius are actually the same value let center = Kernel::get_kernel_center(size); - let min_radius = ((size - 1) / 2).pow(2); // min radius is from center to border let max_radius = center * center + center * center; // max radius is from center to corner @@ -62,28 +67,10 @@ impl Kernel { pub fn is_valid_radius(size: usize, radius_sqr: usize) -> bool { let (min_radius, max_radius) = Kernel::get_valid_radius_bounds(size); - min_radius <= radius_sqr && radius_sqr <= max_radius } - // pub fn get_min_circularity(size: usize, radius_limit: f32) -> f32 { - // let center = Kernel::get_kernel_center(size); - // - // let min_radius = (size - 1) as f32 / 2.0; - // let max_radius = f32::sqrt(center * center + center * center); - // - // let actual_max_radius = f32::min(max_radius, radius_limit); // get LOWER bound - // - // // calculate circularity which results in actual max radius by linear combination of min - // // and max radius - // // a=xb+(1-x)c => x = (a-c)/(b-c) - // - // let min_circularity = (actual_max_radius - max_radius) / (min_radius - max_radius); - // - // min_circularity - // } - /// TODO: this could also be further optimized by using the kernels symmetry, but instead of /// optimizing this function it would make sense to replace the entire kernel fn get_kernel_vector(size: usize, radius_sqr: usize) -> Array2 { @@ -126,11 +113,42 @@ impl Kernel { valid_sqr_distances } - pub fn evaluate_kernels(max_kernel_size: usize) { - let all_valid_radii_sqr = Kernel::get_unique_radii_sqr(max_kernel_size, false); + pub fn check_kernel_configuration(inner_kernel: &Kernel, outer_kernel: &Kernel) -> bool { + let max_offset = inner_kernel.max_offset(); + let inner_center = inner_kernel.center(); + let outer_center = outer_kernel.center(); - // TODO: use two hashmaps to achieve bidirectional mapping, not sure if i actually need - // this, but might come in handy + for x in (0..=max_offset).rev() { + for y in (0..=max_offset).rev() { + let inner_pos = (inner_center + x, inner_center + y); + if inner_kernel.vector.get(inner_pos) != Some(&true) { + continue; // inner cell is not active, skip + } + + // check adjacent neighboring cells in outer kernel + for &offset in &[(1, 1), (0, 1), (0, 1)] { + let outer_pos = (outer_center + x + offset.0, outer_center + y + offset.1); + if outer_kernel.vector.get(outer_pos) != Some(&true) { + return false; // outer cell is not active -> INVALID + } + } + } + } + + true + } + + /// Precomputes all possible unique squared radii that can be used and their compatibility + /// when used as inner and outer kernels. The algorithm that checks the validity is somewhat + /// inefficient, so this function should be called once at startup, but not used at runtime. + /// TODO: currently this returns a bidirectional mapping using two hashmaps. Do i actually need + /// that? + /// TODO: precomputed information could be encapsulated inside a struct and easily accessed + /// using functions, hiding all that pain + pub fn precompute_kernel_configurations( + max_kernel_size: usize, + ) -> (Vec, HashMap, HashMap) { + let all_valid_radii_sqr = Kernel::get_unique_radii_sqr(max_kernel_size, false); let mut max_inner_radius_for_outer: HashMap = HashMap::new(); let mut max_outer_radius_for_inner: HashMap = HashMap::new(); @@ -140,11 +158,13 @@ impl Kernel { for inner_radius_index in (0..outer_radius_index).rev() { let inner_radius = *all_valid_radii_sqr.get(inner_radius_index).unwrap(); - // validate if inner radius is valid TODO: replace this with an error free method! - let factor: f64 = 2.0 * SQRT_2 * outer_radius.to_f64().sqrt(); - let kernel_is_valid = inner_radius.to_f64() <= outer_radius.to_f64() - factor + 2.0; + // validate if inner radius is valid + let inner_kernel = Kernel::new(max_kernel_size, inner_radius); + let outer_kernel = Kernel::new(max_kernel_size, outer_radius); + let kernel_valid = Kernel::check_kernel_configuration(&inner_kernel, &outer_kernel); - if kernel_is_valid { + // if it is optimal, store it as the upper bound and skip all possible smaller ones + if kernel_valid { println!("outer: {:} \t inner: {:}", outer_radius, inner_radius); max_inner_radius_for_outer.insert(outer_radius, inner_radius); // always unique entry max_outer_radius_for_inner.insert(inner_radius, outer_radius); // will override @@ -153,8 +173,11 @@ impl Kernel { } } - dbg!(max_inner_radius_for_outer); - dbg!(max_outer_radius_for_inner); + ( + all_valid_radii_sqr, + max_inner_radius_for_outer, + max_outer_radius_for_inner, + ) } }