Spaces:
Sleeping
Sleeping
Commit
·
1294291
1
Parent(s):
5e404a3
Add computational math lab and benchmarking features
Browse filesIntroduces a new 'Math Lab' mode in the app with random graph generation and Bellman-Ford algorithm benchmarking. Adds experiments.py for scientific graph generation and benchmarking utilities, and extends Graph class with adjacency matrix conversion and population methods.
- app.py +65 -0
- experiments.py +133 -0
- graph_module.py +40 -1
app.py
CHANGED
|
@@ -3,11 +3,13 @@ import graphviz
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import pandas as pd
|
|
|
|
| 6 |
from datetime import datetime
|
| 7 |
import google.generativeai as genai
|
| 8 |
from graph_module import Graph
|
| 9 |
from algorithms import bellman_ford_list
|
| 10 |
from leads_manager import get_analytics
|
|
|
|
| 11 |
|
| 12 |
# --- CONFIG ---
|
| 13 |
st.set_page_config(layout="wide", page_title="SellMe AI Engine")
|
|
@@ -358,6 +360,69 @@ def draw_graph(graph_data, current_node, predicted_path):
|
|
| 358 |
|
| 359 |
# --- MAIN APP ---
|
| 360 |
st.sidebar.title("🛠️ SellMe Control")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
# --- API KEY SETUP (Robust) ---
|
| 363 |
api_key = None
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import pandas as pd
|
| 6 |
+
import time
|
| 7 |
from datetime import datetime
|
| 8 |
import google.generativeai as genai
|
| 9 |
from graph_module import Graph
|
| 10 |
from algorithms import bellman_ford_list
|
| 11 |
from leads_manager import get_analytics
|
| 12 |
+
import experiments
|
| 13 |
|
| 14 |
# --- CONFIG ---
|
| 15 |
st.set_page_config(layout="wide", page_title="SellMe AI Engine")
|
|
|
|
| 360 |
|
| 361 |
# --- MAIN APP ---
|
| 362 |
st.sidebar.title("🛠️ SellMe Control")
|
| 363 |
+
app_mode = st.sidebar.selectbox("Mode", ["Sales Bot Demo", "🧪 Math Lab"])
|
| 364 |
+
|
| 365 |
+
if app_mode == "🧪 Math Lab":
|
| 366 |
+
st.title("🧪 Computational Math Lab")
|
| 367 |
+
|
| 368 |
+
tab1, tab2 = st.tabs(["🎲 Random Graph", "⚡ Performance"])
|
| 369 |
+
|
| 370 |
+
with tab1:
|
| 371 |
+
st.header("Random Graph Generation (Erdős-Rényi)")
|
| 372 |
+
c1, c2 = st.columns(2)
|
| 373 |
+
n = c1.slider("Number of Vertices (N)", 5, 50, 10, step=5)
|
| 374 |
+
density = c2.slider("Edge Density", 0.1, 1.0, 0.3)
|
| 375 |
+
|
| 376 |
+
if st.button("Generate Graph"):
|
| 377 |
+
# Generate
|
| 378 |
+
graph = experiments.generate_random_graph(n, density)
|
| 379 |
+
|
| 380 |
+
# Show Adjacency Matrix
|
| 381 |
+
st.subheader("Adjacency Matrix")
|
| 382 |
+
matrix = graph.to_adjacency_matrix()
|
| 383 |
+
# Convert float('inf') to string "∞" for display
|
| 384 |
+
display_matrix = [[("∞" if x == float('inf') else x) for x in row] for row in matrix]
|
| 385 |
+
df_matrix = pd.DataFrame(display_matrix)
|
| 386 |
+
st.dataframe(df_matrix)
|
| 387 |
+
|
| 388 |
+
# Show Graphviz
|
| 389 |
+
st.subheader("Visual Representation")
|
| 390 |
+
# We can reuse draw_graph but need to adapt arguments roughly
|
| 391 |
+
# Creating a fake structure to reuse draw_graph or just drawing simple one here.
|
| 392 |
+
# draw_graph expects (graph, node_to_id, id_to_node, nodes, edges) tuple + current_node + path
|
| 393 |
+
|
| 394 |
+
dot = graphviz.Digraph()
|
| 395 |
+
dot.attr(rankdir='LR')
|
| 396 |
+
for i in range(graph.num_vertices):
|
| 397 |
+
dot.node(str(i), label=f"Node {i}")
|
| 398 |
+
|
| 399 |
+
for u, neighbors in graph.adj_list.items():
|
| 400 |
+
for v, w in neighbors:
|
| 401 |
+
dot.edge(str(u), str(v), label=str(w))
|
| 402 |
+
|
| 403 |
+
st.graphviz_chart(dot)
|
| 404 |
+
|
| 405 |
+
with tab2:
|
| 406 |
+
st.header("Algorithm Benchmarking")
|
| 407 |
+
st.markdown("**Algorithm:** Bellman-Ford (Adjacency List)")
|
| 408 |
+
st.markdown("**Theoretical Complexity:** $O(V \\cdot E)$")
|
| 409 |
+
|
| 410 |
+
bench_density = st.slider("Benchmark Density", 0.1, 1.0, 0.5)
|
| 411 |
+
if st.button("Run Benchmarks 🚀"):
|
| 412 |
+
with st.spinner("Running experiments..."):
|
| 413 |
+
sizes = [10, 50, 100, 200]
|
| 414 |
+
results = experiments.run_benchmark(sizes, bench_density)
|
| 415 |
+
|
| 416 |
+
# Results
|
| 417 |
+
df_res = pd.DataFrame(results, columns=["Vertices", "Time (seconds)"])
|
| 418 |
+
st.table(df_res)
|
| 419 |
+
|
| 420 |
+
# Chart
|
| 421 |
+
st.line_chart(df_res.set_index("Vertices")["Time (seconds)"])
|
| 422 |
+
|
| 423 |
+
st.stop() # Stop here so we don't render the Sales Bot Demo
|
| 424 |
+
|
| 425 |
+
# --- SALES BOT DEMO CONTINUES BELOW ---
|
| 426 |
|
| 427 |
# --- API KEY SETUP (Robust) ---
|
| 428 |
api_key = None
|
experiments.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import time
|
| 3 |
+
import statistics
|
| 4 |
+
from graph_module import Graph
|
| 5 |
+
from algorithms import bellman_ford_list
|
| 6 |
+
|
| 7 |
+
# --- SCIENTIFIC CORE ---
|
| 8 |
+
|
| 9 |
+
def generate_erdos_renyi(n, density):
|
| 10 |
+
"""
|
| 11 |
+
Generates a random directed graph using the Erdős-Rényi model.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
n (int): Number of vertices.
|
| 15 |
+
density (float): Probability of edge creation (0.0 to 1.0).
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
Graph: A generated graph object.
|
| 19 |
+
"""
|
| 20 |
+
graph = Graph(n, directed=True)
|
| 21 |
+
|
| 22 |
+
# Directed graph max edges = n * (n - 1) (no self-loops)
|
| 23 |
+
max_edges = n * (n - 1)
|
| 24 |
+
target_edges = int(max_edges * density)
|
| 25 |
+
|
| 26 |
+
# Track existing edges to avoid duplicates
|
| 27 |
+
existing_edges = set()
|
| 28 |
+
count = 0
|
| 29 |
+
|
| 30 |
+
# Safety: if target_edges > possible edges, clamp it
|
| 31 |
+
if target_edges > max_edges:
|
| 32 |
+
target_edges = max_edges
|
| 33 |
+
|
| 34 |
+
while count < target_edges:
|
| 35 |
+
u = random.randint(0, n - 1)
|
| 36 |
+
v = random.randint(0, n - 1)
|
| 37 |
+
|
| 38 |
+
if u != v: # No self-loops
|
| 39 |
+
edge_key = (u, v)
|
| 40 |
+
if edge_key not in existing_edges:
|
| 41 |
+
existing_edges.add(edge_key)
|
| 42 |
+
weight = random.randint(-2, 10) # Scientific constraint: -2 to 10
|
| 43 |
+
graph.add_edge(u, v, weight)
|
| 44 |
+
count += 1
|
| 45 |
+
|
| 46 |
+
return graph
|
| 47 |
+
|
| 48 |
+
def run_scientific_benchmark(sizes, densities, num_runs=20):
|
| 49 |
+
"""
|
| 50 |
+
Runs rigorous benchmarks for Bellman-Ford algorithm.
|
| 51 |
+
CRITICAL: For each pair (n, d), we repeat the experiment num_runs times.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
sizes (list[int]): List of vertex counts.
|
| 55 |
+
densities (list[float]): List of edge densities.
|
| 56 |
+
num_runs (int): Number of repetitions for averaging.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
list[dict]: List of result dictionaries ready for DataFrame.
|
| 60 |
+
"""
|
| 61 |
+
results = []
|
| 62 |
+
|
| 63 |
+
print(f"--- Scientific Benchmark Started (Runs per config: {num_runs}) ---")
|
| 64 |
+
|
| 65 |
+
for n in sizes:
|
| 66 |
+
for d in densities:
|
| 67 |
+
times = []
|
| 68 |
+
edge_counts = []
|
| 69 |
+
|
| 70 |
+
for _ in range(num_runs):
|
| 71 |
+
# 1. Generate NEW random graph (don't count this in timing)
|
| 72 |
+
graph = generate_erdos_renyi(n, d)
|
| 73 |
+
|
| 74 |
+
# Count actual edges
|
| 75 |
+
# Sum of adjacency list lengths
|
| 76 |
+
e_count = sum(len(graph.adj_list[u]) for u in range(graph.num_vertices))
|
| 77 |
+
edge_counts.append(e_count)
|
| 78 |
+
|
| 79 |
+
# 2. Start Timer
|
| 80 |
+
start_time = time.perf_counter()
|
| 81 |
+
|
| 82 |
+
# 3. Run Algorithm (Source = 0)
|
| 83 |
+
try:
|
| 84 |
+
bellman_ford_list(graph, 0)
|
| 85 |
+
except Exception:
|
| 86 |
+
pass # Ignore errors (e.g. negative cycles in random graphs)
|
| 87 |
+
|
| 88 |
+
# 4. Stop Timer
|
| 89 |
+
end_time = time.perf_counter()
|
| 90 |
+
times.append(end_time - start_time)
|
| 91 |
+
|
| 92 |
+
# Calculate Averages
|
| 93 |
+
avg_time = statistics.mean(times)
|
| 94 |
+
avg_edges = statistics.mean(edge_counts)
|
| 95 |
+
|
| 96 |
+
# 5. Record Data
|
| 97 |
+
results.append({
|
| 98 |
+
"Vertices (N)": n,
|
| 99 |
+
"Density": d,
|
| 100 |
+
"Avg_Time_Sec": avg_time,
|
| 101 |
+
"Edges_Count": int(avg_edges),
|
| 102 |
+
"Runs": num_runs
|
| 103 |
+
})
|
| 104 |
+
print(f"Config N={n}, D={d} -> Avg Time: {avg_time:.6f}s")
|
| 105 |
+
|
| 106 |
+
return results
|
| 107 |
+
|
| 108 |
+
# --- BACKWARD COMPATIBILITY (Aliases) ---
|
| 109 |
+
|
| 110 |
+
def generate_random_graph(n, density):
|
| 111 |
+
"""Alias for generate_erdos_renyi to support legacy code."""
|
| 112 |
+
return generate_erdos_renyi(n, density)
|
| 113 |
+
|
| 114 |
+
def run_benchmark(sizes, density):
|
| 115 |
+
"""
|
| 116 |
+
Alias supporting legacy list[tuple] return format.
|
| 117 |
+
Uses run_scientific_benchmark internally with 1 run for speed/demo.
|
| 118 |
+
"""
|
| 119 |
+
# Wrap density in list, run 1 time (demo mode usually needs speed)
|
| 120 |
+
# The user asked for rigorous benchmark (20 runs) in the new function,
|
| 121 |
+
# but the old function was for a quick demo.
|
| 122 |
+
# Let's do 1 run to keep UI responsive, or 5 for better stability?
|
| 123 |
+
# Let's stick to 1 to match previous "quick" expectation unless specified.
|
| 124 |
+
|
| 125 |
+
raw_results = run_scientific_benchmark(sizes, [density], num_runs=1)
|
| 126 |
+
|
| 127 |
+
# Convert to list of tuples [(N, Time)]
|
| 128 |
+
return [(r["Vertices (N)"], r["Avg_Time_Sec"]) for r in raw_results]
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
# Test
|
| 132 |
+
res = run_scientific_benchmark([10, 50], [0.1, 0.5], num_runs=5)
|
| 133 |
+
print(res)
|
graph_module.py
CHANGED
|
@@ -2,19 +2,58 @@ class Graph:
|
|
| 2 |
def __init__(self, num_vertices, directed=True):
|
| 3 |
self.num_vertices = num_vertices
|
| 4 |
self.directed = directed
|
| 5 |
-
|
|
|
|
| 6 |
self.adj_list = {i: [] for i in range(num_vertices)}
|
| 7 |
|
| 8 |
def add_edge(self, u, v, weight):
|
| 9 |
if 0 <= u < self.num_vertices and 0 <= v < self.num_vertices:
|
|
|
|
| 10 |
self.adj_list[u].append((v, weight))
|
| 11 |
self.adj_matrix[u][v] = weight
|
|
|
|
| 12 |
if not self.directed:
|
| 13 |
self.adj_list[v].append((u, weight))
|
| 14 |
self.adj_matrix[v][u] = weight
|
| 15 |
else:
|
| 16 |
raise ValueError(f"Vertex index out of bounds: {u}, {v}")
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def get_matrix(self):
|
| 19 |
return self.adj_matrix
|
| 20 |
|
|
|
|
| 2 |
def __init__(self, num_vertices, directed=True):
|
| 3 |
self.num_vertices = num_vertices
|
| 4 |
self.directed = directed
|
| 5 |
+
# Using float('inf') for no connection as requested
|
| 6 |
+
self.adj_matrix = [[float('inf')] * num_vertices for _ in range(num_vertices)]
|
| 7 |
self.adj_list = {i: [] for i in range(num_vertices)}
|
| 8 |
|
| 9 |
def add_edge(self, u, v, weight):
|
| 10 |
if 0 <= u < self.num_vertices and 0 <= v < self.num_vertices:
|
| 11 |
+
# Directed edge logic
|
| 12 |
self.adj_list[u].append((v, weight))
|
| 13 |
self.adj_matrix[u][v] = weight
|
| 14 |
+
|
| 15 |
if not self.directed:
|
| 16 |
self.adj_list[v].append((u, weight))
|
| 17 |
self.adj_matrix[v][u] = weight
|
| 18 |
else:
|
| 19 |
raise ValueError(f"Vertex index out of bounds: {u}, {v}")
|
| 20 |
|
| 21 |
+
def to_adjacency_matrix(self):
|
| 22 |
+
"""
|
| 23 |
+
Returns a 2D list (matrix) of size V x V.
|
| 24 |
+
matrix[u][v] = weight if edge exists.
|
| 25 |
+
matrix[u][v] = float('inf') if no edge exists.
|
| 26 |
+
matrix[u][u] = 0 (distance to self).
|
| 27 |
+
"""
|
| 28 |
+
# Create a deep copy to avoid modifying internal state if needed,
|
| 29 |
+
# or just return internal state if we maintain it strictly.
|
| 30 |
+
# But user requested "matrix[u][u] = 0". Our internal init uses float('inf').
|
| 31 |
+
# So we should probably update internal or return a modified copy.
|
| 32 |
+
# Let's return a generated one if we want to be safe, or update internal.
|
| 33 |
+
# Updating internal is better for consistency if we use it.
|
| 34 |
+
# But wait, internal initialized with inf.
|
| 35 |
+
# Let's just fix diagonals in internal if they are inf.
|
| 36 |
+
|
| 37 |
+
for i in range(self.num_vertices):
|
| 38 |
+
self.adj_matrix[i][i] = 0
|
| 39 |
+
|
| 40 |
+
return self.adj_matrix
|
| 41 |
+
|
| 42 |
+
def from_adjacency_matrix(self, matrix):
|
| 43 |
+
"""
|
| 44 |
+
Clears the current graph.
|
| 45 |
+
Populates the adjacency list based on the matrix values.
|
| 46 |
+
"""
|
| 47 |
+
self.num_vertices = len(matrix)
|
| 48 |
+
self.adj_matrix = matrix
|
| 49 |
+
self.adj_list = {i: [] for i in range(self.num_vertices)}
|
| 50 |
+
|
| 51 |
+
for i in range(self.num_vertices):
|
| 52 |
+
for j in range(self.num_vertices):
|
| 53 |
+
w = matrix[i][j]
|
| 54 |
+
if i != j and w != float('inf'):
|
| 55 |
+
self.adj_list[i].append((j, w))
|
| 56 |
+
|
| 57 |
def get_matrix(self):
|
| 58 |
return self.adj_matrix
|
| 59 |
|