Skip to content
Snippets Groups Projects
Commit 0be5cc4f authored by Valtteri Virta's avatar Valtteri Virta
Browse files

working version

parent ff715cdd
Branches
No related tags found
No related merge requests found
File added
No preview for this file type
No preview for this file type
No preview for this file type
import heapq #min heap
from typing import Dict, Optional, Tuple, List, Any, Iterator
from graph import Graph
from node import Node
def dijkstra_all_steps(graph: Graph,start_id: int) -> Iterator[Tuple[int, int, Dict[int, Optional[int]], Dict[int, Optional[List[int]]]]]:
"""
Yields (step, settled_node_id, distances, routes) each time a node is settled.
- step: 0-based index of the settlement
- settled_node_id: the node whose shortest distance was just finalized
- distances: map node_id -> distance (int) or None if unreachable so far
- routes: map node_id -> list of node_ids from start to that node, or None
"""
# Build lookup
id_map: Dict[int, Node] = {n.id: n for n in graph.get_nodes()}
if start_id not in id_map:
raise KeyError(f"Start node {start_id} not in graph")
start = id_map[start_id]
# Init
dist_f: Dict[Node, float] = {n: float('inf') for n in id_map.values()}
prev: Dict[Node, Optional[Node]] = {n: None for n in id_map.values()}
dist_f[start] = 0.0
# Heap
heap: List[Tuple[float,int,Node]] = [(0.0, start.id, start)]
settled = set()
step = 0
# Main loop
while heap:
# Get node with the smallest distance
d_u, _, u = heapq.heappop(heap)
# Skip if shorter path already found
if d_u > dist_f[u]:
continue
# Mark as settled
settled.add(u)
# Prepare the snapshot of distances + routes
# distances map
distances: Dict[int, Optional[int]] = {}
for n, d in dist_f.items():
distances[n.id] = None if d == float('inf') else int(d)
# reconstruct each route via `prev`
routes: Dict[int, Optional[List[int]]] = {}
for n in id_map.values():
if dist_f[n] == float('inf'):
routes[n.id] = None
else:
# backtrack
path: List[int] = []
cur: Optional[Node] = n
while cur is not None:
path.append(cur.id)
cur = prev[cur]
path.reverse()
routes[n.id] = path
yield step, u.id, distances, routes
step += 1
# Relax neighbors
for v, w in u.get_neighbors():
alt = d_u + w
if alt < dist_f[v]:
dist_f[v] = alt
prev[v] = u
heapq.heappush(heap, (alt, v.id, v))
def dijkstra_steps(graph: Graph,
start_id: int,
target_id: Optional[int] = None) -> Iterator[Tuple[str, Any]]:
# Build a map from IDs to Node objects
id_map: Dict[int, Node] = {node.id: node for node in graph.get_nodes()}
# Check starting id
if start_id not in id_map:
raise KeyError(f"Start node {start_id} not found in graph")
start = id_map[start_id]
target = id_map[target_id]
# Initialize distance and previous-node maps
distances: Dict[Node, float] = {node: float('inf') for node in id_map.values()}
previous: Dict[Node, Optional[Node]] = {node: None for node in id_map.values()}
distances[start] = 0.0
heap: List[Tuple[float, int, Node]] = [(0.0, start.id, start)]
# (set up same as before: id_map, distances, previous, heap...)
while heap:
dist_u, _, u = heapq.heappop(heap)
if dist_u > distances[u]:
continue
yield ('visit', u)
if target_id is not None and u.id == target_id:
break
for v, w in u.get_neighbors():
newd = dist_u + w
if newd < distances[v]:
distances[v] = newd
previous[v] = u
heapq.heappush(heap, (newd, v.id, v))
yield ('relax', u, v, int(newd))
yield ('settle', u)
Dijkstra-algoritmi lyhyesti toimii seuraavasti:
Alustus
- Aseta lähdesolmulle etäisyys 0 ja kaikille muille solmuille etäisyys ∞.
- Merkitse kaikki solmut käsittelemättömiksi.
Valinnan silmukka
- Valitse käsittelemättömistä solmuista se, jolla on pienin etäisyysarvo (esim. min‑keko auttaa tässä).
- Merkitse valittu solmu käsitellyksi.
- Käy läpi valitun solmun kaikki naapurit ja “rentouta” kaaret: jos polun etäisyys valitusta solmusta naapuriin on pienempi kuin aiemmin tallennettu etäisyys, päivitä naapurin etäisyys ja edeltäjäsolmu.
Päättyminen
- Toista edellistä askelta, kunnes kaikki solmut on käsitelty tai kun pienin käsittelemätön etäisyys on ∞ (eli muille solmuille ei ole yhteyttä).
Tulokset
- Lopuksi taulukosta löytyy kunkin solmun lyhyin etäisyys lähdesolmusta ja halutessa koko polku kulkemalla solmujen edeltäjiä taaksepäin.
Huom. Algoritmi toimii vain ei-negatiivisilla kaaripainoilla ja tuottaa yksittäisen lähdesolmun lyhimmät polut kaikkiin muihin solmuihin.
\ No newline at end of file
......@@ -14,7 +14,6 @@ class Graph:
Add a new node to the graph or return the existing one.
"""
# Check if node is already added -> no doubles
if node.id not in self.nodes:
self.nodes[node.id] = node
# Return added node
......
......@@ -8,9 +8,7 @@ class GraphGenerator:
Generates random graphs based on specified parameters.
"""
@staticmethod
def generate(num_nodes: int,
density: float,
weight_range: Tuple[int, int]) -> Graph:
def generate(num_nodes: int,density: float,weight_range: Tuple[int, int]) -> Graph:
"""
Generate a random undirected graph.
"""
......@@ -40,15 +38,11 @@ class GraphGenerator:
return graph
@staticmethod
def connect_by_distance(
graph: Graph,
positions: Dict[Node, Tuple[int, int]],
density: float = 1.0
) -> None:
def connect_by_distance(graph: Graph,positions: Dict[Node, Tuple[int, int]],density: float = 1.0) -> None:
"""
For every pair of nodes in `graph`, with probability `density`,
compute weight = Euclidean distance (based on `positions`) and
add an undirected edge with that weight.
For each unique node pair, with probability `density`,
add an undirected edge whose weight is the Euclidean distance
(scaled by 1/100 and rounded).
"""
nodes = list(graph.get_nodes())
for i in range(len(nodes)):
......
import tkinter as tk
from typing import Dict, Tuple
from tkinter import ttk, messagebox
import random
import math
from graph import Graph
from node import Node
from graphGenerator import GraphGenerator
from dijkstra import dijkstra_all_steps
class GraphGUI:
"""
A Tkinter GUI for generating and displaying spatial graphs
with controlled edge counts per node.
Widgets:
- Entry for number of nodes.
- Entry for minimum spacing between nodes.
- Entry for minimum edges per node.
- Entry for maximum edges per node.
- Button to generate graph.
- Canvas to draw nodes and edges with weights (rounded distance/100).
A Tkinter GUI for generating spatial graphs and visualizing
Dijkstra's algorithm step-by-step with a results table,
with selectable weight mode (distance-based vs. random).
"""
def __init__(self, master: tk.Tk,
canvas_size: int = 600,
node_radius: int = 15) -> None:
def __init__(self, master: tk.Tk, canvas_size: int = 600, node_radius: int = 15) -> None:
self.master = master
master.title("Graph Viewer")
master.title("Graph Dijkstra Visualizer")
self.canvas_size = canvas_size
self.node_radius = node_radius
self.min_distance = node_radius * 4 # default spatial spacing
self.min_distance = node_radius * 4 # default spacing
self.graph: Graph = None
self.positions: Dict[Node, Tuple[int, int]] = {}
self.positions: dict[Node, tuple[int,int]] = {}
self._dij_steps = None # generator for Dijkstra steps
# Canvas for drawing
self.canvas = tk.Canvas(master,
width=canvas_size,
height=canvas_size,
bg="white")
self.canvas.pack(side=tk.LEFT, padx=5, pady=5)
# Canvas
self.canvas = tk.Canvas(master,width=canvas_size,height=canvas_size,bg="white")
self.canvas.grid(row=0, column=0, rowspan=6, padx=5, pady=5)
# Control panel
control = tk.Frame(master)
control.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)
control.grid(row=0, column=1, sticky="nw", padx=10, pady=5)
tk.Label(control, text="Number of nodes:").pack(anchor='w')
self.nodes_entry = tk.Entry(control)
# Number of nodes
tk.Label(control, text="Number of nodes:").grid(row=0, column=0, sticky="w")
self.nodes_entry = tk.Entry(control, width=10)
self.nodes_entry.insert(0, "10")
self.nodes_entry.pack(fill='x')
self.nodes_entry.grid(row=0, column=1)
tk.Label(control, text="Min node spacing:").pack(anchor='w', pady=(10,0))
self.spacing_entry = tk.Entry(control)
# Min node spacing
tk.Label(control, text="Min node spacing:").grid(row=1, column=0, sticky="w", pady=(5,0))
self.spacing_entry = tk.Entry(control, width=10)
self.spacing_entry.insert(0, str(self.min_distance))
self.spacing_entry.pack(fill='x')
self.spacing_entry.grid(row=1, column=1, pady=(5,0))
tk.Label(control, text="Edge density (0.0 - 1.0):").pack(anchor='w', pady=(10,0))
self.density_entry = tk.Entry(control)
# Edge density
tk.Label(control, text="Edge density (0–1):").grid(row=2, column=0, sticky="w", pady=(5,0))
self.density_entry = tk.Entry(control, width=10)
self.density_entry.insert(0, "0.2")
self.density_entry.pack(fill='x')
self.density_entry.grid(row=2, column=1, pady=(5,0))
tk.Button(control,
text="Generate Controlled Graph",
command=self.generate_graph).pack(pady=20)
# Weight mode selector
tk.Label(control, text="Weight mode:").grid(row=3, column=0, sticky="w", pady=(10,0))
self.weight_var = tk.StringVar(value="distance")
tk.Radiobutton(control,
text="Distance-based",
variable=self.weight_var,
value="distance",
command=self._on_weight_mode_change)\
.grid(row=3, column=1, sticky="w", pady=(10,0))
tk.Radiobutton(control,
text="Random",
variable=self.weight_var,
value="random",
command=self._on_weight_mode_change)\
.grid(row=4, column=1, sticky="w")
def generate_graph(self) -> None:
# Min/Max weight (for random mode)
self.minw_label = tk.Label(control, text="Min weight:")
self.minw_entry = tk.Entry(control, width=10)
self.minw_entry.insert(0, "1")
self.maxw_label = tk.Label(control, text="Max weight:")
self.maxw_entry = tk.Entry(control, width=10)
self.maxw_entry.insert(0, "10")
# Place them initially, then hide/show in _on_weight_mode_change
self.minw_label.grid(row=5, column=0, sticky="w", pady=(5,0))
self.minw_entry.grid(row=5, column=1, pady=(5,0))
self.maxw_label.grid(row=6, column=0, sticky="w", pady=(5,0))
self.maxw_entry.grid(row=6, column=1, pady=(5,0))
self._on_weight_mode_change()
# Generate graph button
tk.Button(control,text="Generate Graph",command=self.generate_graph).grid(row=7, column=0, columnspan=2, pady=10, sticky="we")
# Separator
sep = ttk.Separator(control, orient="horizontal")
sep.grid(row=8, column=0, columnspan=2, sticky="we", pady=5)
# Start node for Dijkstra
tk.Label(control, text="Start node ID:").grid(row=9, column=0, sticky="w")
self.start_entry = tk.Entry(control, width=10)
self.start_entry.grid(row=9, column=1)
tk.Button(control, text="Start Dijkstra", command=self.start_dijkstra).grid(row=10, column=0, columnspan=2, pady=(5,10), sticky="we")
tk.Button(control, text="Next Step", command=self.next_step).grid(row=11, column=0, columnspan=2, pady=(0,10), sticky="we")
# Results table
table_frame = tk.Frame(master)
table_frame.grid(row=12, column=0, columnspan=2, padx=5, pady=5, sticky="nsew")
self.tree = ttk.Treeview(
table_frame,
columns=("Node","Distance","Route"),
show="headings",
height=10
)
self.tree.heading("Node", text="Node")
self.tree.heading("Distance", text="Distance")
self.tree.heading("Route", text="Route")
self.tree.column("Node", width=60, anchor="center")
self.tree.column("Distance", width=80, anchor="center")
self.tree.column("Route", width=200, anchor="w")
self.tree.tag_configure('settled', background='lightgreen')
self.tree.tag_configure('unreachable', background='lightgray')
self.tree.tag_configure('normal', background='white')
self.tree.pack(fill="both", expand=True)
num_nodes = int(self.nodes_entry.get())
density = float(self.density_entry.get())
# Track canvas items for recoloring
self._node_items: dict[Node,int] = {}
self._edge_items: dict[tuple[int,int], int] = {}
def _on_weight_mode_change(self) -> None:
"""Show or hide min/max weight entries based on weight mode."""
if self.weight_var.get() == "random":
self.minw_label.grid()
self.minw_entry.grid()
self.maxw_label.grid()
self.maxw_entry.grid()
else:
self.minw_label.grid_remove()
self.minw_entry.grid_remove()
self.maxw_label.grid_remove()
self.maxw_entry.grid_remove()
def generate_graph(self) -> None:
"""Generate and draw a graph according to the selected weight mode."""
try:
n = int(self.nodes_entry.get())
d = float(self.density_entry.get())
self.min_distance = int(self.spacing_entry.get())
except ValueError:
messagebox.showerror("Input error", "Please enter valid numbers.")
return
mode = self.weight_var.get()
if mode == "random":
# random-weighted graph
try:
min_w = int(self.minw_entry.get())
max_w = int(self.maxw_entry.get())
except ValueError:
messagebox.showerror("Input error", "Enter valid min/max weights.")
return
self.graph = GraphGenerator.generate(n, d, (min_w, max_w))
self._assign_positions()
else:
# distance-weighted graph
self.graph = Graph()
nodes = [Node(i) for i in range(num_nodes)]
nodes = [Node(i) for i in range(n)]
for node in nodes:
self.graph.add_node(node)
self._assign_positions()
GraphGenerator.connect_by_distance(self.graph, self.positions, d)
GraphGenerator.connect_by_distance(self.graph, self.positions, density)
self._dij_steps = None
self.tree.delete(*self.tree.get_children())
self._draw_graph()
def _assign_positions(self) -> None:
"""
Assign random positions for each node within the canvas,
ensuring a minimum distance between any two nodes.
"""
"""Place nodes randomly with minimum spacing."""
self.positions.clear()
margin = self.node_radius + 10
for node in self.graph.get_nodes():
placed = False
for _ in range(1000):
# pick a random point within the drawable area
x = random.randint(margin, self.canvas_size - margin)
y = random.randint(margin, self.canvas_size - margin)
# ensure this point is at least self.min_distance from all existing nodes
if all(math.hypot(x-px, y-py) >= self.min_distance
for px,py in self.positions.values()):
self.positions[node] = (x,y)
placed = True
break
if not placed:
# if no place just place it
else:
self.positions[node] = (x,y)
def _draw_graph(self) -> None:
"""
Draw edges with weights in red, then nodes as blue circles.
"""
"""Render nodes and edges, storing canvas IDs for later recoloring."""
self.canvas.delete("all")
self._node_items.clear()
self._edge_items.clear()
# First draw edges, so they don't show on top of nodes
# Draw edges from Graph data
for src in self.graph.get_nodes():
for dst, weight in src.get_neighbors():
if src.id < dst.id:
x1, y1 = self.positions[src]
x2, y2 = self.positions[dst]
self.canvas.create_line(x1, y1, x2, y2)
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
for u in self.graph.get_nodes():
for v,w in u.get_neighbors():
if u.id < v.id:
x1,y1 = self.positions[u]
x2,y2 = self.positions[v]
eid = self.canvas.create_line(x1,y1, x2,y2, fill="gray")
midx,midy = (x1+x2)/2, (y1+y2)/2
self.canvas.create_text(
mx, my,
text=str(weight),
fill="red",
font=("Helvetica", 16, "bold")
midx, midy,
text=str(w),
fill="red", font=("Helvetica",16,"bold")
)
self._edge_items[(u.id,v.id)] = eid
# Draw nodes
for node,(x,y) in self.positions.items():
r = self.node_radius
self.canvas.create_oval(
x - r, y - r, x + r, y + r,
fill="lightblue"
)
nid = self.canvas.create_oval(x-r,y-r, x+r,y+r, fill="lightblue")
self.canvas.create_text(x,y, text=str(node.id))
self._node_items[node] = nid
def start_dijkstra(self) -> None:
"""Reset styling and initialize the Dijkstra step generator."""
if not self.graph:
messagebox.showwarning("No graph", "Generate a graph first.")
return
try:
sid = int(self.start_entry.get())
except ValueError:
messagebox.showerror("Input error", "Enter a valid start node ID.")
return
for nid in self._node_items.values():
self.canvas.itemconfig(nid, fill="lightblue")
for eid in self._edge_items.values():
self.canvas.itemconfig(eid, fill="gray", width=1)
self._dij_steps = dijkstra_all_steps(self.graph, sid)
self.tree.delete(*self.tree.get_children())
self.next_step()
def next_step(self) -> None:
"""Advance one Dijkstra step, recolor node, and update the table."""
if not self._dij_steps:
return
try:
step, settled_id, distances, routes = next(self._dij_steps)
except StopIteration:
messagebox.showinfo("Done", "Dijkstra complete.")
return
# Highlight settled node
for node in self.graph.get_nodes():
if node.id == settled_id:
self.canvas.itemconfig(self._node_items[node], fill="green")
break
# Refresh table
self.tree.delete(*self.tree.get_children())
for nid in sorted(distances):
dist = distances[nid]
route = routes[nid]
dist_str = str(dist) if dist is not None else ""
route_str = "".join(map(str, route)) if route else ""
if nid == settled_id:
tag = 'settled'
elif dist is None:
tag = 'unreachable'
else:
tag = 'normal'
self.tree.insert("", "end",
values=(nid, dist_str, route_str),
tags=(tag,))
root = tk.Tk()
app = GraphGUI(root)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment