From 8d2391b3e2f41a6ceb91d78f68176d72694c1fa0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 19:06:34 +0000 Subject: [PATCH] Add GUI app to find closed debt chains Co-authored-by: skornilov1957 --- main.py | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..b0a8675 --- /dev/null +++ b/main.py @@ -0,0 +1,279 @@ +import random +import time +import tkinter as tk +from tkinter import messagebox, ttk + +EDGE_DENSITY = 0.3 +MAX_N = 300 +MAX_M = 10 +MAX_ESTIMATED_EXPANSIONS = 2_000_000 +MAX_DISPLAY_CHAINS = 50 + + +def center_window(root, width, height): + root.update_idletasks() + screen_width = root.winfo_screenwidth() + screen_height = root.winfo_screenheight() + x = max((screen_width - width) // 2, 0) + y = max((screen_height - height) // 2, 0) + root.geometry(f"{width}x{height}+{x}+{y}") + + +def validate_inputs(n, m, d1, d2): + if n < 2: + return "n must be at least 2." + if m < 2: + return "m must be at least 2." + if m > n: + return "m cannot be greater than n." + if n > MAX_N: + return f"n exceeds limit {MAX_N}." + if m > MAX_M: + return f"m exceeds limit {MAX_M}." + if d1 < 0 or d2 < 0: + return "d1 and d2 must be non-negative." + if d1 > d2: + return "d1 must be less than or equal to d2." + return None + + +def generate_random_debts(n, d1, d2): + adj = [[] for _ in range(n)] + weights = [{} for _ in range(n)] + if n <= 1: + return adj, weights + + max_degree = max(1, int((n - 1) * EDGE_DENSITY)) + all_nodes = list(range(n)) + for i in range(n): + candidates = all_nodes[:i] + all_nodes[i + 1 :] + k = random.randint(1, max_degree) + neighbors = random.sample(candidates, k) + for j in neighbors: + amount = random.randint(d1, d2) + adj[i].append((j, amount)) + weights[i][j] = amount + + incoming = [0] * n + for i in range(n): + for j, _ in adj[i]: + incoming[j] += 1 + for node in range(n): + if incoming[node] == 0: + src = random.choice([i for i in range(n) if i != node]) + if node not in weights[src]: + amount = random.randint(d1, d2) + adj[src].append((node, amount)) + weights[src][node] = amount + incoming[node] += 1 + + return adj, weights + + +def estimate_expansions(n, avg_out_degree, m): + if m <= 1 or avg_out_degree <= 0: + return 0 + return n * (avg_out_degree ** (m - 1)) + + +def find_cycles_length_m(adj, weights, m, display_limit): + n = len(adj) + count = 0 + display = [] + visited = [False] * n + path = [] + + for start in range(n): + visited[start] = True + path[:] = [start] + + def dfs(current, depth, min_so_far): + nonlocal count + if depth == m: + if start in weights[current]: + min_amount = min(min_so_far, weights[current][start]) + count += 1 + if len(display) < display_limit: + display.append((path[:] + [start], min_amount)) + return + + for neighbor, amount in adj[current]: + if neighbor <= start or visited[neighbor]: + continue + visited[neighbor] = True + path.append(neighbor) + dfs(neighbor, depth + 1, min(min_so_far, amount)) + path.pop() + visited[neighbor] = False + + dfs(start, 1, float("inf")) + visited[start] = False + + return count, display + + +class DebtChainFinderApp: + def __init__(self, root): + self.root = root + self.root.title("Closed Debt Chains Finder") + center_window(self.root, 940, 680) + + style = ttk.Style(self.root) + if "clam" in style.theme_names(): + style.theme_use("clam") + + main_frame = ttk.Frame(self.root, padding=10) + main_frame.grid(row=0, column=0, sticky="nsew") + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + + input_frame = ttk.LabelFrame(main_frame, text="Parameters", padding=10) + input_frame.grid(row=0, column=0, sticky="ew") + input_frame.columnconfigure(1, weight=1) + + self.n_var = tk.StringVar(value="30") + self.m_var = tk.StringVar(value="4") + self.d1_var = tk.StringVar(value="10") + self.d2_var = tk.StringVar(value="100") + + ttk.Label(input_frame, text="n (companies):").grid( + row=0, column=0, sticky="w", padx=4, pady=4 + ) + ttk.Entry(input_frame, textvariable=self.n_var, width=12).grid( + row=0, column=1, sticky="w", padx=4, pady=4 + ) + + ttk.Label(input_frame, text="m (chain length):").grid( + row=0, column=2, sticky="w", padx=4, pady=4 + ) + ttk.Entry(input_frame, textvariable=self.m_var, width=12).grid( + row=0, column=3, sticky="w", padx=4, pady=4 + ) + + ttk.Label(input_frame, text="d1 (min debt):").grid( + row=1, column=0, sticky="w", padx=4, pady=4 + ) + ttk.Entry(input_frame, textvariable=self.d1_var, width=12).grid( + row=1, column=1, sticky="w", padx=4, pady=4 + ) + + ttk.Label(input_frame, text="d2 (max debt):").grid( + row=1, column=2, sticky="w", padx=4, pady=4 + ) + ttk.Entry(input_frame, textvariable=self.d2_var, width=12).grid( + row=1, column=3, sticky="w", padx=4, pady=4 + ) + + self.run_button = ttk.Button( + input_frame, text="Generate and Find Chains", command=self.on_run + ) + self.run_button.grid(row=0, column=4, rowspan=2, padx=8, pady=4, sticky="ns") + + limits_text = ( + f"System limits: n <= {MAX_N}, m <= {MAX_M}, " + f"estimated expansions <= {MAX_ESTIMATED_EXPANSIONS:,}" + ) + ttk.Label(main_frame, text=limits_text).grid( + row=1, column=0, sticky="w", padx=4, pady=6 + ) + + self.summary_var = tk.StringVar(value="Chains found: - | Time: -") + ttk.Label(main_frame, textvariable=self.summary_var).grid( + row=2, column=0, sticky="w", padx=4, pady=2 + ) + + self.warning_var = tk.StringVar(value="") + self.warning_label = tk.Label(main_frame, textvariable=self.warning_var, fg="red") + self.warning_label.grid(row=3, column=0, sticky="w", padx=4, pady=2) + + output_frame = ttk.LabelFrame(main_frame, text="Results", padding=6) + output_frame.grid(row=4, column=0, sticky="nsew", pady=(8, 0)) + main_frame.rowconfigure(4, weight=1) + output_frame.rowconfigure(0, weight=1) + output_frame.columnconfigure(0, weight=1) + + self.results_text = tk.Text(output_frame, height=20, wrap="word") + self.results_text.grid(row=0, column=0, sticky="nsew") + scrollbar = ttk.Scrollbar( + output_frame, orient="vertical", command=self.results_text.yview + ) + scrollbar.grid(row=0, column=1, sticky="ns") + self.results_text.configure(yscrollcommand=scrollbar.set) + + def _update_results(self, text): + self.results_text.configure(state="normal") + self.results_text.delete("1.0", tk.END) + self.results_text.insert(tk.END, text) + self.results_text.configure(state="disabled") + + def _show_error(self, message): + self.warning_var.set(message) + messagebox.showerror("Input error", message) + + def on_run(self): + self.warning_var.set("") + try: + n = int(self.n_var.get()) + m = int(self.m_var.get()) + d1 = int(self.d1_var.get()) + d2 = int(self.d2_var.get()) + except ValueError: + self._show_error("All inputs must be integers.") + return + + error = validate_inputs(n, m, d1, d2) + if error: + self._show_error(error) + return + + self.summary_var.set("Working...") + self._update_results("") + self.root.update_idletasks() + + start_time = time.perf_counter() + adj, weights = generate_random_debts(n, d1, d2) + edge_count = sum(len(edges) for edges in adj) + avg_out_degree = edge_count / n if n else 0.0 + estimate = estimate_expansions(n, avg_out_degree, m) + + if estimate > MAX_ESTIMATED_EXPANSIONS: + elapsed = time.perf_counter() - start_time + message = ( + f"Estimated search size {estimate:,.0f} exceeds the system limit " + f"{MAX_ESTIMATED_EXPANSIONS:,}. Reduce n or m." + ) + self.warning_var.set(message) + messagebox.showwarning("Limitations exceeded", message) + self.summary_var.set(f"Aborted due to limitations | Time: {elapsed:.4f}s") + return + + count, display = find_cycles_length_m(adj, weights, m, MAX_DISPLAY_CHAINS) + elapsed = time.perf_counter() - start_time + + lines = [ + f"Generated graph: {n} companies, {edge_count} debts, " + f"avg out-degree {avg_out_degree:.2f}", + f"Cycle length m: {m}", + ] + if count == 0: + lines.append("No closed chains found.") + else: + lines.append(f"Showing {len(display)} of {count} chains:") + for idx, (cycle, min_amount) in enumerate(display, 1): + formatted = " -> ".join(f"C{node + 1}" for node in cycle) + lines.append( + f"{idx:02d}. {formatted} | min compensation: {min_amount}" + ) + + self._update_results("\n".join(lines)) + self.summary_var.set(f"Chains found: {count} | Time: {elapsed:.4f}s") + + +def main(): + root = tk.Tk() + app = DebtChainFinderApp(root) + root.mainloop() + + +if __name__ == "__main__": + main()