Romanchello-bit commited on
Commit
1294291
·
1 Parent(s): 5e404a3

Add computational math lab and benchmarking features

Browse files

Introduces 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.

Files changed (3) hide show
  1. app.py +65 -0
  2. experiments.py +133 -0
  3. 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
- self.adj_matrix = [[None] * num_vertices for _ in range(num_vertices)]
 
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