Skip to content

Commit

Permalink
Add new algorithm for checking kernel validity
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
iMilchshake committed Mar 12, 2024
1 parent 1ef4c50 commit df31e17
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 96 deletions.
76 changes: 13 additions & 63 deletions src/kernel_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
89 changes: 56 additions & 33 deletions src/map.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::f64::consts::SQRT_2;



use crate::CuteWalker;
use crate::Position;
use egui::emath::Numeric;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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<bool> {
Expand Down Expand Up @@ -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<usize>, HashMap<usize, usize>, HashMap<usize, usize>) {
let all_valid_radii_sqr = Kernel::get_unique_radii_sqr(max_kernel_size, false);
let mut max_inner_radius_for_outer: HashMap<usize, usize> = HashMap::new();
let mut max_outer_radius_for_inner: HashMap<usize, usize> = HashMap::new();

Expand All @@ -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
Expand All @@ -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,
)
}
}

Expand Down

0 comments on commit df31e17

Please sign in to comment.