Refactor layouting into separate crate

This commit is contained in:
hal8174 2025-01-27 22:09:59 +01:00
parent c3bb980fcf
commit 5c8010c23b
15 changed files with 124 additions and 55 deletions

View file

@ -24,7 +24,6 @@ image = "0.25.2"
miette = { version = "7.2.0", features = ["fancy"] }
proptest = "1.5.0"
proptest-derive = "0.5.0"
rand = { version = "0.8.5", features = ["small_rng"] }
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
serde_yaml = "0.9.34"

View file

@ -1,44 +0,0 @@
size:
x: 15
y: 15
blocks:
- size:
x: 3
y: 2
input:
- offset:
x: 1
y: 1
dir: Up
output:
- offset:
x: 1
y: 0
dir: Up
- size:
x: 5
y: 2
input:
output:
- offset:
x: 1
y: 1
dir: Down
- size:
x: 5
y: 7
input:
- offset:
x: 0
y: 1
dir: Right
output:
connections:
- startblock: 1
startpoint: 0
endblock: 0
endpoint: 0
- startblock: 0
startpoint: 0
endblock: 2
endpoint: 0

View file

@ -1,61 +0,0 @@
size:
x: 30
y: 30
blocks:
- size:
x: 3
y: 2
input:
- offset:
x: 1
y: 1
dir: Up
output:
- offset:
x: 1
y: 0
dir: Up
- size:
x: 5
y: 2
input:
output:
- offset:
x: 1
y: 1
dir: Down
- size:
x: 5
y: 7
input:
- offset:
x: 0
y: 1
dir: Right
output:
- size:
x: 5
y: 5
input:
- offset:
x: 0
y: 1
dir: Right
output:
- offset:
x: 0
y: 3
dir: Left
connections:
- startblock: 1
startpoint: 0
endblock: 0
endpoint: 0
- startblock: 0
startpoint: 0
endblock: 3
endpoint: 0
- startblock: 3
startpoint: 0
endblock: 2
endpoint: 0

View file

@ -1,115 +0,0 @@
size:
x: 50
y: 50
blocks:
- size:
x: 3
y: 2
input:
- offset:
x: 1
y: 1
dir: Up
output:
- offset:
x: 1
y: 0
dir: Up
- size:
x: 5
y: 2
input:
output:
- offset:
x: 1
y: 1
dir: Down
- size:
x: 5
y: 7
input:
- offset:
x: 0
y: 1
dir: Right
output:
- size:
x: 5
y: 5
input:
- offset:
x: 0
y: 1
dir: Right
output:
- offset:
x: 0
y: 3
dir: Left
- size:
x: 10
y: 10
input:
- offset:
x: 0
y: 1
dir: Right
- offset:
x: 0
y: 3
dir: Right
output:
- offset:
x: 9
y: 1
dir: Right
- offset:
x: 9
y: 3
dir: Right
- size:
x: 10
y: 5
input:
- offset:
x: 0
y: 1
dir: Right
- offset:
x: 0
y: 3
dir: Right
output:
- offset:
x: 9
y: 1
dir: Right
- offset:
x: 9
y: 3
dir: Right
connections:
- startblock: 1
startpoint: 0
endblock: 0
endpoint: 0
- startblock: 0
startpoint: 0
endblock: 3
endpoint: 0
- startblock: 3
startpoint: 0
endblock: 4
endpoint: 0
- startblock: 4
startpoint: 0
endblock: 5
endpoint: 0
- startblock: 4
startpoint: 1
endblock: 5
endpoint: 1
- startblock: 5
startpoint: 0
endblock: 2
endpoint: 0

View file

@ -1,6 +1,5 @@
use crate::graph::wheighted_graph::shortest_path::dijkstra;
use crate::graph::wheighted_graph::WheightedGraph;
use crate::layout::Layout;
use crate::misc::Map;
use crate::priority_queue::BinaryHeap;
use factorio_core::{prelude::*, visualize::Visualize};
@ -34,44 +33,44 @@ impl Problem {
}
}
pub fn from_layout(l: &Layout) -> Self {
let mut p = Self::new(l.problem.size.x as usize, l.problem.size.y as usize);
// pub fn from_layout(l: &Layout) -> Self {
// let mut p = Self::new(l.problem.size.x as usize, l.problem.size.y as usize);
for b in &l.blocks {
let aabb = b.get_aabb();
// for b in &l.blocks {
// let aabb = b.get_aabb();
p.set_blocked_range(
aabb.min().x as usize,
aabb.min().y as usize,
aabb.max().x as usize,
aabb.max().y as usize,
true,
);
}
// p.set_blocked_range(
// aabb.min().x as usize,
// aabb.min().y as usize,
// aabb.max().x as usize,
// aabb.max().y as usize,
// true,
// );
// }
for c in &l.problem.connections {
let start_transform = l.blocks[c.startblock].block_to_world();
let startpos = l.problem.blocks[c.startblock].output[c.startpoint]
.offset
.transform(start_transform);
let startdir = l.problem.blocks[c.startblock].output[c.startpoint]
.dir
.transform(start_transform);
let end_transform = l.blocks[c.endblock].block_to_world();
let endpos = l.problem.blocks[c.endblock].input[c.endpoint]
.offset
.transform(end_transform);
let enddir = l.problem.blocks[c.endblock].input[c.endpoint]
.dir
.transform(end_transform);
p.add_connection(
(startpos, startdir),
(endpos.in_direction(&enddir, -1), enddir),
);
}
// for c in &l.problem.connections {
// let start_transform = l.blocks[c.startblock].block_to_world();
// let startpos = l.problem.blocks[c.startblock].output[c.startpoint]
// .offset
// .transform(start_transform);
// let startdir = l.problem.blocks[c.startblock].output[c.startpoint]
// .dir
// .transform(start_transform);
// let end_transform = l.blocks[c.endblock].block_to_world();
// let endpos = l.problem.blocks[c.endblock].input[c.endpoint]
// .offset
// .transform(end_transform);
// let enddir = l.problem.blocks[c.endblock].input[c.endpoint]
// .dir
// .transform(end_transform);
// p.add_connection(
// (startpos, startdir),
// (endpos.in_direction(&enddir, -1), enddir),
// );
// }
p
}
// p
// }
pub fn add_connection(&mut self, start: (Position, Direction), end: (Position, Direction)) {
self.start.push(start);

View file

@ -1,98 +0,0 @@
use clap::{Parser, Subcommand};
use factorio_core::visualize::Visualize;
use factorio_pathfinding::layout::{genetic_algorithm2, GeneticAlgorithm, PathLayout};
use miette::{Context, IntoDiagnostic, Result};
use rand::{rngs::SmallRng, SeedableRng};
use std::path::PathBuf;
#[derive(Debug, Parser)]
struct Args {
#[clap(short, long, default_value_t = 0)]
seed: u64,
problem: PathBuf,
#[command(subcommand)]
subcommand: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
V1,
V2,
Bench {
#[clap(short, long, default_value_t = 100)]
runs: usize,
#[clap(short, long, default_value_t = 100)]
mutations: usize,
},
}
fn main() -> Result<()> {
let args = Args::parse();
let mut rng = SmallRng::seed_from_u64(args.seed);
let file = std::fs::File::open(args.problem)
.into_diagnostic()
.context("Failed to open problem file.")?;
let p = serde_yaml::from_reader(file)
.into_diagnostic()
.context("Failed to parse yaml.")?;
match args.subcommand {
Commands::V1 => {
let mut g = GeneticAlgorithm::new(&p, 20, 2, 0, &mut rng);
for i in 0..100 {
println!("Generatrion {i}");
g.generation(&mut rng);
}
g.output_population();
}
Commands::V2 => {
let mut m: Option<PathLayout> = None;
for _ in 0..20 {
let g = genetic_algorithm2(&p, 10, 320, 10000, &mut rng);
g.print_visualization();
if m.as_ref().is_none_or(|m| g.score() < m.score()) {
m = Some(g);
}
}
m.unwrap().print_visualization();
}
Commands::Bench { runs, mutations } => {
let mut map = Vec::new();
let mut m: Option<PathLayout> = None;
let start = std::time::Instant::now();
for _ in 0..runs {
let g = genetic_algorithm2(&p, 1, mutations, mutations, &mut rng);
map.push(g.score());
if m.as_ref().is_none_or(|m| g.score() < m.score()) {
m = Some(g);
}
}
println!("Time: {:.2}", start.elapsed().as_secs_f32());
let mean = map.iter().sum::<usize>() / runs;
println!("Mean: {}", mean);
let min = map.iter().min().unwrap();
println!("Min: {}", min);
let max = map.iter().max().unwrap();
println!("Max: {}", max);
let stddev =
((map.iter().map(|v| (v - mean) * (v - mean)).sum::<usize>() / runs) as f64).sqrt();
println!("Stddev: {:.1}", stddev);
m.unwrap().print_visualization();
}
}
Ok(())
}

View file

@ -1,574 +0,0 @@
use crate::belt_finding::conflict_avoidance::ConflictAvoidance;
use factorio_core::{
pathfield::PathField,
prelude::*,
visualize::{image_grid, Color, Symbol, Visualization, Visualize},
};
use rand::{seq::SliceRandom, Rng};
use serde::{Deserialize, Serialize};
use std::{sync::atomic::AtomicU32, time::Instant};
static OUTFILEINDEX: AtomicU32 = AtomicU32::new(0);
pub struct GeneticAlgorithm<'a> {
problem: &'a Problem,
population: Vec<PathLayout<'a>>,
population_size: usize,
population_keep: usize,
population_new: usize,
}
impl<'a> GeneticAlgorithm<'a> {
pub fn new<R: Rng + ?Sized>(
problem: &'a Problem,
population_size: usize,
population_keep: usize,
population_new: usize,
rng: &mut R,
) -> GeneticAlgorithm<'a> {
let mut population = Vec::new();
let start = Instant::now();
let mut count: usize = 0;
while population.len() < population_size {
count += 1;
if let Some(p) = PathLayout::new(Layout::new(problem, rng)) {
population.push(p);
}
}
println!("Layouts accepted: {}/{}", population_size, count);
population.sort_by_key(|p| p.score());
println!(
"Best score: {}. Time: {:.2}s",
population[0].score(),
start.elapsed().as_secs_f32()
);
population[0].print_visualization();
GeneticAlgorithm {
problem,
population,
population_size,
population_keep,
population_new,
}
}
pub fn generation<R: Rng + ?Sized>(&mut self, rng: &mut R) {
let start_new = Instant::now();
for i in self.population_keep..(self.population_keep + self.population_new) {
loop {
if let Some(p) = PathLayout::new(Layout::new(self.problem, rng)) {
self.population[i] = p;
break;
}
}
}
let duration_new = start_new.elapsed();
let start_mutate = Instant::now();
for i in (self.population_keep + self.population_new)..self.population_size {
let j = i - (self.population_keep + self.population_new);
loop {
if let Some(p) =
PathLayout::new(self.population[j % self.population_keep].layout.mutate(rng))
{
self.population[i] = p;
break;
}
}
}
let duration_mutate = start_mutate.elapsed();
self.population.sort_by_key(|p| p.score());
println!(
"Best score: {}. Time new: {:.2}s. Time mutate: {:.2}s",
self.population[0].score(),
duration_new.as_secs_f32(),
duration_mutate.as_secs_f32()
);
self.population[0].print_visualization();
let v: Vec<_> = self.population.iter().map(|p| p.visualize()).collect();
let img = image_grid(&v, v[0].size().x as u32, v[0].size().y as u32, 5);
let mut file = std::fs::File::create("generation.png").unwrap();
img.write_to(&mut file, image::ImageFormat::Png).unwrap();
}
pub fn output_population(&self) {
println!("Population:");
for (i, p) in self.population.iter().enumerate() {
println!("{i:3}: {}", p.score());
p.print_visualization();
}
}
}
pub fn genetic_algorithm2<'a, R: Rng + ?Sized>(
problem: &'a Problem,
new_layouts: usize,
mutation_timeout: usize,
max_mutations: usize,
rng: &'_ mut R,
) -> PathLayout<'a> {
let mut m = (0..new_layouts)
.map(|_| valid_path_layout(problem, rng))
.min_by_key(|p| p.score())
.unwrap();
// m.print_visualization();
let mut last_improvement = 0;
let mut count = 0;
while last_improvement < mutation_timeout && count < max_mutations {
last_improvement += 1;
count += 1;
if let Some(p) = PathLayout::new(m.layout.mutate(rng)) {
if p.score() < m.score() {
m = p;
// println!("Step: {count}");
// m.print_visualization();
last_improvement = 0;
}
}
}
m
}
pub fn valid_path_layout<'a, R: Rng + ?Sized>(
problem: &'a Problem,
rng: &'_ mut R,
) -> PathLayout<'a> {
loop {
if let Some(p) = PathLayout::new(Layout::new(problem, rng)) {
return p;
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct MacroBlock {
pub(crate) size: Position,
pub(crate) input: Vec<Interface>,
pub(crate) output: Vec<Interface>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Interface {
pub(crate) offset: Position,
pub(crate) dir: Direction,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Connection {
pub(crate) startblock: usize,
pub(crate) startpoint: usize,
pub(crate) endblock: usize,
pub(crate) endpoint: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Problem {
pub(crate) size: Position,
pub(crate) blocks: Vec<MacroBlock>,
pub(crate) connections: Vec<Connection>,
}
// #[derive(Debug, Clone, Copy)]
// pub struct BlockHandle(usize);
#[derive(Debug, Clone)]
pub struct Layout<'a> {
pub(crate) problem: &'a Problem,
pub(crate) blocks: Vec<Block>,
}
pub struct PathLayout<'a> {
layout: Layout<'a>,
paths: Vec<Vec<PathField>>,
score: usize,
}
impl<'a> PathLayout<'a> {
pub fn new(layout: Layout<'a>) -> Option<PathLayout<'a>> {
let mut p = crate::belt_finding::Problem::from_layout(&layout);
if !p.find_path() {
return None;
}
let mut c = ConflictAvoidance::new(&p);
let start = std::time::Instant::now();
if !c.remove_all_conflicts(Some(std::time::Duration::from_secs(2))) {
if start.elapsed().as_secs_f32() > 0.5 {
// println!("Conflict avoidance: {:.2}", start.elapsed().as_secs_f32());
// c.print_visualization();
let file = std::fs::File::create(format!(
"out/{}.json",
OUTFILEINDEX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
))
.unwrap();
serde_json::to_writer(file, &p).unwrap();
// println!("Saved slow solve.");
}
return None;
}
let paths = c.get_paths().to_vec();
let score = paths
.iter()
.map(|path| path.iter().skip(1).map(|p| p.cost()).sum::<usize>())
.sum();
Some(PathLayout {
layout,
paths,
score,
})
}
pub fn score(&self) -> usize {
self.score
}
}
impl Visualize for PathLayout<'_> {
fn visualize(&self) -> factorio_core::visualize::Visualization {
let mut v = self.layout.visualize();
let offset = self.layout.blocks.len();
for (i, path) in self.paths.iter().enumerate() {
for p in &path[1..] {
match p {
PathField::Belt { pos, dir } => {
v.add_symbol(
*pos,
Symbol::Arrow(*dir),
Some(Color::index(i + offset)),
None,
);
}
PathField::Underground { pos, dir, len } => {
v.add_symbol(
*pos,
Symbol::ArrowEnter(*dir),
Some(Color::index(i + offset)),
None,
);
v.add_symbol(
pos.in_direction(dir, *len as i32),
Symbol::ArrowEnter(*dir),
Some(Color::index(i + offset)),
None,
);
}
}
}
}
v
}
}
impl Problem {
pub fn new(size: Position) -> Self {
Self {
size,
blocks: Vec::new(),
connections: Vec::new(),
}
}
// pub fn add_block(&mut self, size: Position) -> BlockHandle {
// self.blocks.push(Block {
// size,
// input: Vec::new(),
// output: Vec::new(),
// });
// BlockHandle(self.blocks.len() - 1)
// }
// pub fn add_connection(
// &mut self,
// starthandle: BlockHandle,
// startoffset: Position,
// endhandle: BlockHandle,
// endoffset: Position,
// ) {
// let startinterface = self.blocks[starthandle.0].output.len();
// let endinterface = self.blocks[endhandle.0].input.len();
// self.blocks[starthandle.0].output.push(Interface {
// offset: startoffset,
// target: (endhandle.0, endinterface),
// });
// self.blocks[endhandle.0].input.push(Interface {
// offset: endoffset,
// target: (starthandle.0, startinterface),
// })
// }
}
impl Layout<'_> {
/// Create a new valid layout
pub fn new<'a, R: Rng + ?Sized>(problem: &'a Problem, rng: &'_ mut R) -> Layout<'a> {
let mut blocks = Vec::new();
assert!(Self::place_block(problem, &mut blocks, rng));
let mut l = Layout { problem, blocks };
l.center();
l
}
fn place_block<R: Rng + ?Sized>(
problem: &'_ Problem,
blocks: &mut Vec<Block>,
rng: &'_ mut R,
) -> bool {
if problem.blocks.len() == blocks.len() {
return true;
}
let b = &problem.blocks[blocks.len()];
for _ in 0..1000 {
let dir = rng.gen::<Direction>();
let pos = match dir {
Direction::Up => Position::new(
rng.gen_range(0..=(problem.size.x - b.size.x)),
rng.gen_range(0..=(problem.size.y - b.size.y)),
),
Direction::Right => Position::new(
rng.gen_range((b.size.y - 1)..problem.size.x),
rng.gen_range(0..=(problem.size.y - b.size.x)),
),
Direction::Down => Position::new(
rng.gen_range((b.size.x - 1)..problem.size.x),
rng.gen_range((b.size.y - 1)..problem.size.y),
),
Direction::Left => Position::new(
rng.gen_range(0..=(problem.size.x - b.size.y)),
rng.gen_range((b.size.x - 1)..problem.size.y),
),
};
let current = Block::new(pos, dir, problem.blocks[blocks.len()].size);
let current_aabb = current.get_aabb();
if blocks
.iter()
.all(|b| !AABB::collision(b.get_aabb(), current_aabb))
{
blocks.push(current);
if Self::place_block(problem, blocks, rng) {
return true;
}
blocks.pop();
}
}
false
}
pub fn get_aabb(&self) -> AABB {
self.blocks
.iter()
.map(|b| b.get_aabb())
.reduce(AABB::combine)
.expect("At least one block is required.")
}
pub fn center(&mut self) {
let aabb = self.get_aabb();
let rest = self.problem.size - aabb.size();
let new_min = Position::new(rest.x / 2, rest.y / 2);
let t = Transformation::new(Direction::Up, new_min - aabb.min());
for b in &mut self.blocks {
*b = b.transform(t);
}
}
/// Mutate existing layout, creating a valid layout
pub fn mutate<R: Rng + ?Sized>(&self, rng: &mut R) -> Self {
let mut s = self.clone();
#[allow(clippy::type_complexity)]
let r: &[(&dyn Fn(&mut Layout, &mut R) -> bool, _)] = &[
(&Self::mutate_replace::<R>, 30),
(&Self::mutate_flip::<R>, 50),
(&Self::mutate_jiggle::<R>, 160),
];
loop {
let p = r.choose_weighted(rng, |i| i.1).unwrap();
if p.0(&mut s, rng) && rng.gen_bool(0.5) {
break;
}
}
s.center();
s
}
fn mutate_replace<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.gen_range(0..layout.blocks.len());
let dir = rng.gen::<Direction>();
let b = &layout.problem.blocks[i];
let pos = match dir {
Direction::Up => Position::new(
rng.gen_range(0..=(layout.problem.size.x - b.size.x)),
rng.gen_range(0..=(layout.problem.size.y - b.size.y)),
),
Direction::Right => Position::new(
rng.gen_range((b.size.y - 1)..layout.problem.size.x),
rng.gen_range(0..=(layout.problem.size.y - b.size.x)),
),
Direction::Down => Position::new(
rng.gen_range((b.size.x - 1)..layout.problem.size.x),
rng.gen_range((b.size.y - 1)..layout.problem.size.y),
),
Direction::Left => Position::new(
rng.gen_range(0..=(layout.problem.size.x - b.size.y)),
rng.gen_range((b.size.x - 1)..layout.problem.size.y),
),
};
let current = Block::new(pos, dir, b.size);
let current_aabb = current.get_aabb();
if layout
.blocks
.iter()
.enumerate()
.all(|(j, b)| j == i || !AABB::collision(b.get_aabb(), current_aabb))
{
layout.blocks[i] = current;
true
} else {
false
}
}
fn mutate_flip<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.gen_range(0..layout.blocks.len());
let b = &mut layout.blocks[i];
let block = &layout.problem.blocks[i];
let new_pos = match b.dir() {
Direction::Up => b.pos() + block.size - Position::new(1, 1),
Direction::Right => b.pos() + Position::new(1 - block.size.y, block.size.x - 1),
Direction::Down => b.pos() - block.size + Position::new(1, 1),
Direction::Left => b.pos() + Position::new(block.size.y - 1, 1 - block.size.x),
};
let new_dir = b.dir().reverse();
*b = Block::new(new_pos, new_dir, b.size());
true
}
fn mutate_jiggle<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.gen_range(0..layout.blocks.len());
let dir = rng.gen::<Direction>();
let step = [(1, 10), (2, 5), (3, 5)]
.choose_weighted(rng, |i| i.1)
.unwrap()
.0;
// let step = 1;
let b = &layout.blocks[i];
let current = Block::new(b.pos().in_direction(&dir, step), b.dir(), b.size());
let current_aabb = current.get_aabb();
if current_aabb.min().x < 0
|| current_aabb.min().y < 0
|| current_aabb.max().x >= layout.problem.size.x
|| current_aabb.max().y >= layout.problem.size.y
{
return false;
}
if layout
.blocks
.iter()
.enumerate()
.all(|(j, b)| j == i || !AABB::collision(b.get_aabb(), current_aabb))
{
layout.blocks[i] = current;
true
} else {
false
}
}
}
impl Visualize for Layout<'_> {
fn visualize(&self) -> Visualization {
let mut v = Visualization::new(self.problem.size);
for (i, (b, mb)) in self
.blocks
.iter()
.zip(self.problem.blocks.iter())
.enumerate()
{
let c = Color::index(i);
let aabb = b.get_aabb();
for x in aabb.min().x..=aabb.max().x {
for y in aabb.min().y..=aabb.max().y {
v.add_symbol(Position::new(x, y), Symbol::Block, Some(c), None);
}
}
v.add_symbol(b.pos(), Symbol::Char('X'), Some(c), None);
let transform = b.block_to_world();
for input in &mb.input {
v.add_symbol(
input.offset.transform(transform),
Symbol::Char('i'),
Some(c),
None,
);
}
for output in &mb.output {
v.add_symbol(
output.offset.transform(transform),
Symbol::Char('o'),
Some(c),
None,
);
}
}
v
}
}

View file

@ -1,5 +1,4 @@
pub mod belt_finding;
pub mod graph;
pub mod layout;
pub mod misc;
pub mod priority_queue;