Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
neoteristis committed Feb 24, 2024
2 parents 08fe30f + 7e51847 commit 27f8919
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 110 deletions.
41 changes: 41 additions & 0 deletions gui/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging
from pathlib import Path


class Logger:
logger: logging.Logger
log_folder = Path(__file__).parent / "logs"

def __init__(self, clazz):
if not self.log_folder.exists():
self.log_folder.mkdir()

self.logger = logging.getLogger(type(clazz).__name__)
self.logger.setLevel(logging.DEBUG)

self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

self.stream_handler = logging.StreamHandler()
self.stream_handler.setLevel(logging.INFO)
self.stream_handler.setFormatter(self.formatter)
self.logger.addHandler(self.stream_handler)

self.file_handler = logging.FileHandler(self.log_folder / f"{type(clazz).__name__}.log")
self.file_handler.setLevel(logging.DEBUG)
self.file_handler.setFormatter(self.formatter)
self.logger.addHandler(self.file_handler)

def debug(self, message):
self.logger.debug(message)

def info(self, message):
self.logger.info(message)

def warning(self, message):
self.logger.warning(message)

def error(self, message):
self.logger.error(message)

def critical(self, message):
self.logger.critical(message)
35 changes: 26 additions & 9 deletions gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ def generate_board(size: int, pattern_count: int) -> (str, Path, Path):
return board.to_csv(), unsorted_board, board.image


def generate_statistics(start_size: int, end_size: int, start_pattern_count: int, end_pattern_count: int, timeout: float = 4, progress=gr.Progress(track_tqdm=True)) -> Path:
runner = Runner((start_size, end_size), (start_pattern_count, end_pattern_count), timeout=timeout)

results = runner.solve_boards(progress)

grapher = Grapher(results)
return grapher.plot_time_over_size_and_pattern_count_plotly()


image_gen = gr.Interface(
fn=board_to_image,
inputs=["text", "file"],
Expand All @@ -44,18 +53,26 @@ def generate_board(size: int, pattern_count: int) -> (str, Path, Path):
inputs=[
gr.Slider(2, 32, 12, step=1, label="Puzzle Size"),
gr.Slider(1, 128, 22, step=1, label="Pattern count"),
# gr.Dataset(components=["slider"], samples=[["4x4 (2)", "8x8 (4)", "14x14 (22)", "15x15 (22)", "16x16 (22)", "Eternity II (16x16 22)"]])
],
outputs=[gr.Textbox(label="Board Data", show_copy_button=True), "image", "image"]
)

interface = gr.TabbedInterface([board_gen, image_gen], ["Generate Board", "Generate Image"])

if __name__ == "__main__":
runner = Runner((8, 8), (2, 22))
statistics_gen = gr.Interface(
fn=generate_statistics,
inputs=[
gr.Slider(2, 32, 2, step=1, label="Minimum Size"),
gr.Slider(2, 32, 4, step=1, label="Maximum Size"),
gr.Slider(2, 32, 2, step=1, label="Minimum Pattern Count"),
gr.Slider(2, 32, 10, step=1, label="Maximum Pattern Count"),
gr.Slider(0.1, 60, 4, label="Timeout (seconds)")
],
outputs=[
gr.Plot(label="Time over Size and Pattern Count")
]
)

results = runner.solve_boards()
grapher = Grapher(results)
grapher.plot_time_over_pattern_count()
interface = gr.TabbedInterface([board_gen, image_gen, statistics_gen],
["Generate Board", "Generate Image", "Generate Statistics"])

# interface.launch(share=True)
if __name__ == "__main__":
interface.launch(share=True)
16 changes: 14 additions & 2 deletions gui/solverlib/execution.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
from logger import Logger


class Execution:
def __init__(self, logs: str):
def __init__(self, logs: str, timeout: float = None):
self.logger = Logger(self)
self.logs = logs
self.time = 0
self.parse_logs()

if timeout:
self.time = timeout
else:
self.parse_logs()

def parse_logs(self):
# scrape metadata logs
for line in self.logs.split("\n"):
self.logger.debug(f"Scraping line: {line}")
if line.startswith("=time="):
self.logger.debug(f"Found time line: {line}")
self.time = float(line.split("=")[2])

self.logger.debug(f"Execution time: {self.time}")

def __repr__(self):
return f"""
Execution:
Expand Down
20 changes: 11 additions & 9 deletions gui/solverlib/solver.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
import subprocess
import logging
from pathlib import Path

from eternitylib.board import Board
from logger import Logger
from solverlib.execution import Execution


class Solver:
def __init__(self):
# exe is in the same dir as this file
self.logger = Logger(self)
self.exe_path = Path(__file__).parent / "e2solver"

def link_solver(self, path: Path = Path(__file__).parent.parent.parent / "cmake-build-debug/solver/Solver"):
# If file does not exist, raise an error
if not path.exists():
self.logger.error(f"Solver executable not found at {path}")
raise FileNotFoundError(f"Solver executable not found at {path}")

if self.exe_path.exists():
self.logger.debug(f"Deleting existing solver at {path}")
self.exe_path.unlink()

self.logger.debug(f"Symlinking {path} to {self.exe_path}.")
self.exe_path.symlink_to(path)

def solve(self, board: Board, timeout: int = 60) -> Execution:
def solve(self, board: Board, timeout: float = 60) -> Execution:
# Write the board to a file
self.logger.info(f"Writing board to tmp/board.{board.hash()}.txt.")
board_path = Path(__file__).parent.parent / f"tmp/board.{board.hash()}.txt"
board_path.parent.mkdir(parents=True, exist_ok=True)
board_path.write_text(board.to_csv())

# print info to log
logging.
# Run the solver
try:
logging.info(f"Running solver with timeout of {timeout} seconds")
self.logger.info(f"Running solver with timeout of {timeout} seconds")
result = subprocess.run([self.exe_path, str(board_path)], capture_output=True, timeout=timeout)
logging.info("Solver finished.")
self.logger.info(f"Solver returned with code {result.returncode}")
except subprocess.TimeoutExpired:
logging.warning(f"Solver timed out after {timeout} seconds")
return Execution("")
self.logger.warning(f"Solver timed out after {timeout} seconds")
return Execution("", timeout=timeout)
return Execution(result.stdout.decode("utf-8"))
30 changes: 30 additions & 0 deletions gui/statiscticslib/grapher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import random
import string
from pathlib import Path
from typing import List

import matplotlib.pyplot as plt
import pandas as pd
import plotly.graph_objects as go

from statiscticslib.result import Result

Expand All @@ -9,6 +14,15 @@ class Grapher:
def __init__(self, data: List[Result]):
self.data = data

@property
def df(self):
# Create a dataframe with board size as columns and pattern count as index and time as values
df = pd.DataFrame(columns=list(set([result.board.size for result in self.data])), index=list(set([result.board.pattern_count for result in self.data])))
for result in self.data:
df[result.board.size][result.board.pattern_count] = result.execution.time
return df


def plot_time_over_size(self):
plt.plot([result.board.size for result in self.data], [result.execution.time for result in self.data])
plt.xlabel("Size")
Expand All @@ -34,4 +48,20 @@ def plot_time_over_size_and_pattern_count(self):
ax.set_ylabel("Pattern Count")
ax.set_zlabel("Time")

rand_path = Path(__file__).parent.parent / "tmp" / f"{''.join(random.choices(string.ascii_lowercase, k=10))}.jpg"

plt.show()
plt.savefig(rand_path)
return rand_path

def plot_time_over_size_and_pattern_count_plotly(self) -> str:
fig = go.Figure(data=[go.Surface(z=self.df.values)])

fig.update_layout(scene=dict(
xaxis_title='Size',
yaxis_title='Pattern Count',
zaxis_title='Time'
))

return fig

22 changes: 18 additions & 4 deletions gui/statiscticslib/runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import itertools

import gradio as gr
from tqdm import tqdm

from eternitylib.board import Board
from logger import Logger
from solverlib.solver import Solver
from statiscticslib.result import Result

Expand All @@ -11,30 +15,40 @@ class Runner:
solver: Solver
boards: list[Board]
results: list[Result]
logger: Logger

def __init__(self, size_range=(4, 8), pattern_count_range=(2, 22)):
def __init__(self, size_range=(4, 8), pattern_count_range=(2, 22), timeout: float = 60):
self.logger = Logger(self)
self.solver = Solver()
self.boards = []
self.results = []
self.timeout = timeout

self.set_size_range(*size_range)
self.set_pattern_count_range(*pattern_count_range)
self.logger.info(
f"New Runner of {size_range[0]}x{size_range[0]} to {size_range[1]}x{size_range[1]} size and from {pattern_count_range[0]} to {pattern_count_range[1]} patterns.")

def generate_boards(self):
self.logger.info(f"Generating {len(list(itertools.product(self.sizes, self.pattern_counts)))} boards.")
for size, pattern_count in itertools.product(self.sizes, self.pattern_counts):
self.logger.debug(f"Genrating {size}x{size} of {pattern_count} patterns.")
self.boards.append(Board().generate(size, pattern_count))

def set_size_range(self, size_start, size_end):
self.logger.info(f"Setting runner size from {size_start} to {size_end}.")
self.sizes = list(range(size_start, size_end + 1))

def set_pattern_count_range(self, pattern_count_start, pattern_count_end):
self.logger.info(f"Setting runner pattern count from {pattern_count_start} to {pattern_count_end}.")
self.pattern_counts = list(range(pattern_count_start, pattern_count_end + 1))

def solve_boards(self) -> list[Result]:
def solve_boards(self, progress=gr.Progress(track_tqdm=True)) -> list[Result]:
if not self.boards:
self.generate_boards()

for board in self.boards:
self.results.append(Result(board, self.solver.solve(board)))
for board in tqdm(self.boards):
self.logger.debug(f"Solving {board}.")
self.results.append(Result(board, self.solver.solve(board, timeout=self.timeout)))

return self.results
Loading

0 comments on commit 27f8919

Please sign in to comment.