Skip to content

Commit

Permalink
Python class to manage sc2pathlib usage better and give typing help. …
Browse files Browse the repository at this point in the history
…Methods for adding influence. Also plotting using cv2.
  • Loading branch information
Aki Vänttinen committed Jun 27, 2019
1 parent 105d94d commit 19d7f4a
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Cargo.lock
**/*.rs.bk
sc2pathlib.pyd
sc2pathlib.so
sc2pathlibp/__pycache__/
2 changes: 1 addition & 1 deletion build.bat
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cargo build --release
REM if not exist "sc2pathlib" mkdir sc2pathlib
copy "target\release\sc2pathlib.dll" "sc2pathlib.pyd"
copy "target\release\sc2pathlib.dll" "sc2pathlibp\sc2pathlib.pyd"
pause
1 change: 1 addition & 0 deletions sc2pathlibp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .path_finder import PathFinder
80 changes: 80 additions & 0 deletions sc2pathlibp/path_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from . sc2pathlib import PathFind
#from . import _sc2pathlib
#import sc2pathlib
import numpy as np
from typing import Union, List, Tuple
from math import floor

class PathFinder():
def __init__(self, maze: Union[List[List[int]], np.array]):
"""
pathing values need to be integers to improve performance.
Initialization should be done with array consisting values of 0 and 1.
"""
self._path_find = PathFind(maze)
self.heuristic_accuracy = 0

def normalize_influence(self, value: int):
"""
Normalizes influence to integral value.
Influence does not need to be calculated each frame, but this quickly resets
influence values to specified value without changing available paths.
"""
self._path_find.normalize_influence(value)

@property
def width(self):
self._path_find.width

@property
def height(self):
self._path_find.height

@property
def map(self):
self._path_find.map

def find_path(self, start: (float, float), end: (float, float)) -> Tuple[List[Tuple[int, int]], float]:
start_int = (floor(start[0]), floor(start[1]))
end_int = (floor(end[0]), floor(end[1]))
return self._path_find.find_path(start_int, end_int, self.heuristic_accuracy)

def find_path_influence(self, start: (float, float), end: (float, float)) -> (List[Tuple[int, int]], float):
start_int = (floor(start[0]), floor(start[1]))
end_int = (floor(end[0]), floor(end[1]))
return self._path_find.find_path_influence(start_int, end_int, self.heuristic_accuracy)

def safest_spot(self, destination_center: (float, float), walk_distance: float) -> (Tuple[int, int], float):
destination_int = (floor(destination_center[0]), floor(destination_center[1]))
return self._path_find.lowest_influence_walk(destination_int, walk_distance)

def lowest_influence_in_grid(self, destination_center: (float, float), radius: int) -> (Tuple[int, int], float):
destination_int = (floor(destination_center[0]), floor(destination_center[1]))
return self._path_find.lowest_influence(destination_int, radius)

def add_influence(self, points: List[Tuple[float, float]], value: float, distance: float):
list = []
for point in points:
list.append((floor(point[0]), floor(point[1])))

self._path_find.add_influence(list, value, distance)

def add_influence_walk(self, points: List[Tuple[float, float]], value: float, distance: float):
list = []
for point in points:
list.append((floor(point[0]), floor(point[1])))

self._path_find.add_walk_influence(list, value, distance)

def plot(self, path: List[Tuple[int, int]]):
"""
requires opencv-python
"""
import cv2
image = np.array(self._path_find.map, dtype = np.uint8)
for point in path:
image[point] = 255
image = np.rot90(image, 1)
resized = cv2.resize(image, dsize=None, fx=4, fy=4)
cv2.imshow(f"influence map", resized);
cv2.waitKey(1);
123 changes: 112 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use pyo3::prelude::*;
//use pyo3::wrap_pyfunction;
use pathfinding::prelude::{absdiff, astar, dijkstra_all};
use pathfinding::prelude::{absdiff, astar, dijkstra_all, dijkstra_partial};
use std::cmp::{min, max};
mod pos;

Expand Down Expand Up @@ -45,7 +45,7 @@ impl PathFind {

// object.normal_influence
#[getter(normal_influence)]
fn get_normal_influenceidth(&self)-> PyResult<usize>{
fn get_normal_influence(&self)-> PyResult<usize>{
Ok(self.normal_influence)
}

Expand Down Expand Up @@ -88,17 +88,96 @@ impl PathFind {
}

fn normalize_influence(&mut self, value: usize) {
let height = self.map[0].len();
self.normal_influence = value;

for x in 0..self.map.len() {
for y in 0..height {
for x in 0..self.width {
for y in 0..self.height {
if self.map[x][y] > 0 {
self.map[x][y] = value;
}
}
}
}

/// Adds influence based on euclidean distance
fn add_influence(&mut self, positions: Vec<(usize, usize)>, max: f32, distance: f32) -> PyResult<()> {
let mult = 1.0 / (distance * pos::MULTF64 as f32);

for x in 0..self.width {
for y in 0..self.height {
if self.map[x][y] > 0 {
let mut added_value: usize = 0;

for position in &positions {
let value = max * (1.0 - (quick_distance(*position, (x, y)) as f32) * mult);
if value > 0.0 {
added_value += value as usize;

}
}

self.map[x][y] += added_value;
}
}
}

Ok(())
}

/// Adds influence based on walk distance
fn add_walk_influence(&mut self, positions: Vec<(usize, usize)>, max: f64, distance: f64) -> PyResult<()> {
let mult = 1.0 / distance;
let max_int = max as usize;

for position in &positions {
let destinations = self.find_destinations_in_inline(*position, distance);
self.map[position.0][position.1] += max_int;

for destination in destinations {
let end_point = destination.0;
let current_distance = destination.1;
let value = max * (1.0 - current_distance * mult);

if value > 0.0 {
self.map[end_point.0][end_point.1] += value as usize
}
}
}

Ok(())
}

/// Finds the first reachable position within specified walking distance from the center point with lowest value
fn lowest_influence_walk(&self, center: (usize, usize), distance: f64) -> PyResult<((usize, usize), f64)> {
Ok(self.lowest_influence_walk_inline(center, distance))
}

#[inline]
fn lowest_influence_walk_inline(&self, center: (usize, usize), distance: f64) -> ((usize, usize), f64) {
let destinations = self.find_destinations_in_inline(center, distance);

let mut min_value = std::usize::MAX;
let mut min_distance = std::f64::MAX;
let mut min_position = center.clone();

for destination in destinations {
let pos = destination.0;
let new_val = self.map[pos.0][pos.1];
if new_val == 0 { continue; }

let distance = destination.1;

if new_val < min_value || (new_val == min_value && distance < min_distance) {
min_value = new_val;
min_distance = distance;
min_position = pos;
}
}

(min_position, min_distance)
}


/// Finds the first reachable position within specified distance from the center point with lowest value
fn lowest_influence(&self, center: (f32, f32), distance: usize) -> PyResult<((usize, usize), f64)> {
Ok(self.inline_lowest_value(center, distance))
Expand Down Expand Up @@ -145,12 +224,6 @@ impl PathFind {
let goal: pos::Pos = pos::Pos(end.0, end.1);
let grid: &Vec<Vec<usize>> = &self.map;

let heuristic: Box<dyn Fn(&pos::Pos)->usize> = match possible_heuristic.unwrap_or(0) {
0 => Box::new(|p: &pos::Pos| p.manhattan_distance(&goal)),
1 => Box::new(|p: &pos::Pos| p.quick_distance(&goal)),
_ => Box::new(|p: &pos::Pos| p.euclidean_distance(&goal)),
};

let result: Option<(Vec<pos::Pos>, usize)>;
match possible_heuristic.unwrap_or(0) {
0 => result = astar(&start, |p| p.successors(grid), |p| p.manhattan_distance(&goal), |p| *p == goal),
Expand Down Expand Up @@ -228,6 +301,34 @@ impl PathFind {

Ok(destination_collection)
}

/// Finds all reachable destinations from selected start point. Ignores influence.
fn find_destinations_in(&self, start: (usize, usize), distance: f64 ) -> PyResult<Vec<((usize, usize), f64)>> {
Ok(self.find_destinations_in_inline(start, distance))
}

#[inline]
fn find_destinations_in_inline(&self, start: (usize, usize), distance: f64 ) -> Vec<((usize, usize), f64)> {
let start: pos::Pos = pos::Pos(start.0, start.1);
let grid: &Vec<Vec<usize>> = &self.map;
let u_distance = (distance * pos::MULTF64) as usize;

let result = dijkstra_partial(&start, |p| p.successors(&grid), |p| p.quick_distance(&start) > u_distance);

let hash_map = result.0;
let mut destination_collection: Vec<((usize, usize), f64)> = Vec::<((usize, usize), f64)>::with_capacity(hash_map.len());

for found_path in hash_map {
let x = (found_path.0).0;
let y = (found_path.0).1;
//let x = ((found_path.1).0).0;
//let y = ((found_path.1).0).1;
let d = ((found_path.1).1 as f64) / pos::MULTF64;
destination_collection.push(((x, y), d));
}

destination_collection
}
}


Expand Down
4 changes: 2 additions & 2 deletions src/pos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ impl Pos {
}

pub fn euclidean_distance(&self, other: &Pos) -> usize {
let a = (self.0 - other.0);
let b = (self.1 - other.1);
let a = self.0 - other.0;
let b = self.1 - other.1;
let dist2 = a * a + b * b;
((dist2 as f64).sqrt() * MULTF64) as usize

Expand Down
49 changes: 35 additions & 14 deletions test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sc2pathlib
import sc2pathlibp
import time
from typing import List

Expand All @@ -16,36 +16,40 @@ def read_maze(file_name: str) -> List[List[int]]:
return final_maze

maze = read_maze("tests/maze4x4.txt")
pf = sc2pathlib.PathFind(maze)
pf = sc2pathlibp.PathFinder(maze)
print(pf.map)
print(pf.width)
print(pf.height)

print(pf.find_path((0,0), (0,2)))
pf.normalize_influence(100)
print(pf.lowest_influence((2,2), 5))
print(pf.lowest_influence_in_grid((2,2), 5))
print(pf.find_path((0,0), (0,2)))

maze = read_maze("tests/AutomatonLE.txt")
pf = sc2pathlib.PathFind(maze)
pf = sc2pathlibp.PathFinder(maze)
pf.normalize_influence(10)

result = pf.find_path((32, 51),(150, 129), 0)
pf.heuristic_accuracy = 0
result = pf.find_path((32, 51),(150, 129))
print(f"path distance 0: {result[1]} for path: {result[0]}")

result = pf.find_path((32, 51),(150, 129), 1)
pf.heuristic_accuracy = 1
result = pf.find_path((32, 51),(150, 129))
print(f"path distance 1: {result[1]} for path: {result[0]}")

result = pf.find_path((32, 51),(150, 129), 2)
pf.heuristic_accuracy = 2
result = pf.find_path((32, 51),(150, 129))
print(f"path distance 2: {result[1]} for path: {result[0]}")

result = pf.find_path_influence((32, 51),(150, 129), 0)
pf.heuristic_accuracy = 0
result = pf.find_path_influence((32, 51),(150, 129))
print(f"path influenced distance 0: {result[1]} for path: {result[0]}")

result = pf.find_path_influence((32, 51),(150, 129), 1)
pf.heuristic_accuracy = 1
result = pf.find_path_influence((32, 51),(150, 129))
print(f"path influenced distance 1: {result[1]} for path: {result[0]}")

result = pf.find_path_influence((32, 51),(150, 129), 2)
pf.heuristic_accuracy = 2
result = pf.find_path_influence((32, 51),(150, 129))
print(f"path influenced distance 2: {result[1]} for path: {result[0]}")

expansions = [(29, 65), (35, 34),
Expand All @@ -60,13 +64,30 @@ def read_maze(file_name: str) -> List[List[int]]:
total_distance = 0
ns_pf = time.perf_counter_ns()
pf.normalize_influence(100)
pf.heuristic_accuracy = 0

for pos1 in expansions:
for pos2 in expansions:
result = pf.find_path(pos1, pos2, 2)
result = pf.find_path(pos1, pos2)
total_distance += result[1]

ns_pf = time.perf_counter_ns() - ns_pf

print(f"pathfinding took {ns_pf / 1000 / 1000} ms. Total distance {total_distance}")
print(f"noraml influence: {pf.normal_influence}")

ns_pf = time.perf_counter_ns()
pf.add_influence([(56, 65), (110, 28), (100, 98)], 150, 50)
ns_pf = time.perf_counter_ns() - ns_pf
print(f"adding influence took {ns_pf / 1000 / 1000} ms.")

pf.normalize_influence(100)

ns_pf = time.perf_counter_ns()
pf.add_influence_walk([(56, 65), (110, 28), (100, 98)], 150, 50)
ns_pf = time.perf_counter_ns() - ns_pf
print(f"adding influence by walking distance took {ns_pf / 1000 / 1000} ms.")

result = pf.find_path_influence((29, 65), (154, 114))
print(pf.map)
pf.plot(result[0])
input("Press Enter to continue...")

0 comments on commit 19d7f4a

Please sign in to comment.