Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,7 +10,7 @@ import math
|
|
| 10 |
from datetime import datetime
|
| 11 |
|
| 12 |
# ==========================================
|
| 13 |
-
# 1. JSON & HELPER LOGIC
|
| 14 |
# ==========================================
|
| 15 |
def get_sorted_nodes(G):
|
| 16 |
return sorted(list(G.nodes()), key=lambda l: (l[0], l[1]))
|
|
@@ -430,10 +430,9 @@ IMG_WIDTH_PX = 800
|
|
| 430 |
IMG_HEIGHT_PX = 800
|
| 431 |
|
| 432 |
def plot_graph_to_image(graph, width, height, title="Network"):
|
| 433 |
-
# Create figure with exact pixel control (no margins)
|
| 434 |
dpi = 100
|
| 435 |
fig = plt.figure(figsize=(IMG_WIDTH_PX/dpi, IMG_HEIGHT_PX/dpi), dpi=dpi)
|
| 436 |
-
ax = fig.add_axes([0, 0, 1, 1])
|
| 437 |
|
| 438 |
pos = {node: (node[0], node[1]) for node in graph.nodes()}
|
| 439 |
nx.draw_networkx_edges(graph, pos, ax=ax, width=2, alpha=0.6, edge_color="#333")
|
|
@@ -443,18 +442,12 @@ def plot_graph_to_image(graph, width, height, title="Network"):
|
|
| 443 |
labels = {node: str(i+1) for i, node in enumerate(sorted_nodes)}
|
| 444 |
nx.draw_networkx_labels(graph, pos, labels, ax=ax, font_size=8, font_color="white", font_weight="bold")
|
| 445 |
|
| 446 |
-
# Exact Limits Mapping:
|
| 447 |
-
# Grid goes 0..Width. We want Click (0,0) to be Top-Left.
|
| 448 |
-
# We map 0..Width to X, 0..Height to Y.
|
| 449 |
-
# To center nodes, we set limits -0.5 to Width+0.5
|
| 450 |
ax.set_xlim(-0.5, width + 0.5)
|
| 451 |
-
ax.set_ylim(height + 0.5, -0.5)
|
| 452 |
|
| 453 |
-
# Draw faint grid
|
| 454 |
ax.grid(True, linestyle=':', alpha=0.3)
|
| 455 |
ax.set_axis_on()
|
| 456 |
|
| 457 |
-
# Save to temp file
|
| 458 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
| 459 |
fname = f"temp_plot_{timestamp}.png"
|
| 460 |
plt.savefig(fname)
|
|
@@ -464,50 +457,32 @@ def plot_graph_to_image(graph, width, height, title="Network"):
|
|
| 464 |
def handle_plot_click(evt: gr.SelectData, state_data):
|
| 465 |
if not state_data or "graph" not in state_data: return None, "Generate first.", state_data
|
| 466 |
|
| 467 |
-
# 1. Get click coordinates (pixels)
|
| 468 |
click_x, click_y = evt.index
|
| 469 |
width = state_data["width"]
|
| 470 |
height = state_data["height"]
|
| 471 |
|
| 472 |
-
# 2. Map Pixels -> Grid
|
| 473 |
-
# Logic: Total pixels = 800. Range = Width + 1.
|
| 474 |
-
# NormX = ClickX / 800.
|
| 475 |
-
# DataX = NormX * (Width + 1) - 0.5
|
| 476 |
-
# GridX = round(DataX)
|
| 477 |
-
|
| 478 |
range_x = width + 1.0
|
| 479 |
range_y = height + 1.0
|
| 480 |
-
|
| 481 |
norm_x = click_x / IMG_WIDTH_PX
|
| 482 |
norm_y = click_y / IMG_HEIGHT_PX
|
| 483 |
-
|
| 484 |
-
# Since limits are -0.5 to W+0.5, total span is W+1
|
| 485 |
data_x = norm_x * range_x - 0.5
|
| 486 |
data_y = norm_y * range_y - 0.5
|
| 487 |
-
|
| 488 |
grid_x = int(round(data_x))
|
| 489 |
grid_y = int(round(data_y))
|
| 490 |
|
| 491 |
-
# 3. Perform Action (Smart Toggle)
|
| 492 |
gen = NetworkGenerator(width, height)
|
| 493 |
gen.graph = state_data["graph"]
|
| 494 |
|
| 495 |
-
# Check bounds
|
| 496 |
if 0 <= grid_x <= width and 0 <= grid_y <= height:
|
| 497 |
if gen.graph.has_node((grid_x, grid_y)):
|
| 498 |
-
# Delete
|
| 499 |
success, msg = gen.manual_delete_node(grid_x, grid_y)
|
| 500 |
action = "Deleted"
|
| 501 |
else:
|
| 502 |
-
# Add
|
| 503 |
success, msg = gen.manual_add_node(grid_x, grid_y)
|
| 504 |
action = "Added"
|
| 505 |
else:
|
| 506 |
-
success = False
|
| 507 |
-
msg = "Clicked outside grid."
|
| 508 |
-
action = "Ignored"
|
| 509 |
|
| 510 |
-
# 4. Refresh
|
| 511 |
if success:
|
| 512 |
state_data["graph"] = gen.graph
|
| 513 |
img_path = plot_graph_to_image(gen.graph, width, height)
|
|
@@ -516,10 +491,9 @@ def handle_plot_click(evt: gr.SelectData, state_data):
|
|
| 516 |
else:
|
| 517 |
return gr.update(), f"Error: {msg}", state_data
|
| 518 |
|
| 519 |
-
# ... (Previous helper functions get_preset_dims, update_ui_for_variant, etc. remain same) ...
|
| 520 |
def get_preset_dims(preset_mode, topology):
|
| 521 |
if preset_mode == "Custom": return gr.update(interactive=True), gr.update(interactive=True)
|
| 522 |
-
dims = (6, 11) if topology=="linear" and preset_mode=="Medium" else (8,8)
|
| 523 |
if preset_mode == "Small": dims = (4, 4)
|
| 524 |
if preset_mode == "Large": dims = (16, 16) if topology!="linear" else (10, 26)
|
| 525 |
return gr.update(value=dims[0], interactive=False), gr.update(value=dims[1], interactive=False)
|
|
@@ -540,16 +514,14 @@ def generate_and_store(topology, width, height, variant, void_frac, t_nodes, t_e
|
|
| 540 |
if variant == "Fixed": t_nodes, t_edges = 0, 0
|
| 541 |
gen = NetworkGenerator(width, height, var_code, topology, void_frac, t_nodes, t_edges)
|
| 542 |
graph = gen.generate()
|
| 543 |
-
|
| 544 |
img_path = plot_graph_to_image(graph, width, height)
|
| 545 |
metrics = f"**Nodes:** {len(graph.nodes())} | **Edges:** {len(graph.edges())} | **Density:** {nx.density(graph):.2f}"
|
| 546 |
state_data = { "graph": graph, "width": width, "height": height, "topology": topology }
|
| 547 |
-
return img_path, metrics, state_data, gr.update(interactive=True), gr.update(interactive=True)
|
| 548 |
except Exception as e:
|
| 549 |
-
return None, f"Error: {e}", None, gr.update(interactive=False), gr.update(interactive=False)
|
| 550 |
|
| 551 |
def run_batch_generation(count, topology, width, height, variant, void_frac, t_nodes, t_edges):
|
| 552 |
-
# (Same batch logic as before)
|
| 553 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 554 |
dir_name = f"batch_{timestamp}"
|
| 555 |
os.makedirs(dir_name, exist_ok=True)
|
|
@@ -568,6 +540,15 @@ def run_batch_generation(count, topology, width, height, variant, void_frac, t_n
|
|
| 568 |
except Exception as e:
|
| 569 |
return None
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
# ==========================================
|
| 572 |
# GRADIO UI
|
| 573 |
# ==========================================
|
|
@@ -584,14 +565,20 @@ with gr.Blocks(title="Interactive Network Generator") as demo:
|
|
| 584 |
with gr.Row():
|
| 585 |
width = gr.Number(8, label="Width", interactive=False)
|
| 586 |
height = gr.Number(8, label="Height", interactive=False)
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
void_frac = gr.Slider(0.0, 0.9, 0.35, label="Void Fraction", interactive=False)
|
| 589 |
-
|
| 590 |
-
with gr.Row():
|
| 591 |
-
t_nodes = gr.Number(0, label="Nodes", interactive=False)
|
| 592 |
-
t_edges = gr.Number(0, label="Edges", interactive=False)
|
| 593 |
gen_btn = gr.Button("Generate", variant="primary")
|
| 594 |
-
|
|
|
|
|
|
|
|
|
|
| 595 |
with gr.Tab("Batch"):
|
| 596 |
batch_count = gr.Slider(1, 50, 5, step=1, label="Count")
|
| 597 |
batch_btn = gr.Button("Generate Batch ZIP")
|
|
@@ -599,8 +586,6 @@ with gr.Blocks(title="Interactive Network Generator") as demo:
|
|
| 599 |
|
| 600 |
with gr.Column(scale=2):
|
| 601 |
metrics = gr.Markdown("Ready.")
|
| 602 |
-
# Important: interactive=False prevents user from uploading their own image,
|
| 603 |
-
# but select events still fire.
|
| 604 |
plot_img = gr.Image(label="Interactive Graph", interactive=False, height=800, width=800)
|
| 605 |
|
| 606 |
# EVENTS
|
|
@@ -615,11 +600,14 @@ with gr.Blocks(title="Interactive Network Generator") as demo:
|
|
| 615 |
topology.change(update_ui_for_variant, inputs_var, [void_frac, t_nodes, t_edges])
|
| 616 |
|
| 617 |
gen_args = [topology, width, height, variant, void_frac, t_nodes, t_edges]
|
| 618 |
-
gen_btn.click(generate_and_store, gen_args, [plot_img, metrics, state])
|
| 619 |
|
| 620 |
-
# CLICK EVENT
|
| 621 |
plot_img.select(handle_plot_click, [state], [plot_img, metrics, state])
|
| 622 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
batch_args = [batch_count, topology, width, height, variant, void_frac, t_nodes, t_edges]
|
| 624 |
batch_btn.click(run_batch_generation, batch_args, [file_out])
|
| 625 |
|
|
|
|
| 10 |
from datetime import datetime
|
| 11 |
|
| 12 |
# ==========================================
|
| 13 |
+
# 1. JSON & HELPER LOGIC
|
| 14 |
# ==========================================
|
| 15 |
def get_sorted_nodes(G):
|
| 16 |
return sorted(list(G.nodes()), key=lambda l: (l[0], l[1]))
|
|
|
|
| 430 |
IMG_HEIGHT_PX = 800
|
| 431 |
|
| 432 |
def plot_graph_to_image(graph, width, height, title="Network"):
|
|
|
|
| 433 |
dpi = 100
|
| 434 |
fig = plt.figure(figsize=(IMG_WIDTH_PX/dpi, IMG_HEIGHT_PX/dpi), dpi=dpi)
|
| 435 |
+
ax = fig.add_axes([0, 0, 1, 1])
|
| 436 |
|
| 437 |
pos = {node: (node[0], node[1]) for node in graph.nodes()}
|
| 438 |
nx.draw_networkx_edges(graph, pos, ax=ax, width=2, alpha=0.6, edge_color="#333")
|
|
|
|
| 442 |
labels = {node: str(i+1) for i, node in enumerate(sorted_nodes)}
|
| 443 |
nx.draw_networkx_labels(graph, pos, labels, ax=ax, font_size=8, font_color="white", font_weight="bold")
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
ax.set_xlim(-0.5, width + 0.5)
|
| 446 |
+
ax.set_ylim(height + 0.5, -0.5)
|
| 447 |
|
|
|
|
| 448 |
ax.grid(True, linestyle=':', alpha=0.3)
|
| 449 |
ax.set_axis_on()
|
| 450 |
|
|
|
|
| 451 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
| 452 |
fname = f"temp_plot_{timestamp}.png"
|
| 453 |
plt.savefig(fname)
|
|
|
|
| 457 |
def handle_plot_click(evt: gr.SelectData, state_data):
|
| 458 |
if not state_data or "graph" not in state_data: return None, "Generate first.", state_data
|
| 459 |
|
|
|
|
| 460 |
click_x, click_y = evt.index
|
| 461 |
width = state_data["width"]
|
| 462 |
height = state_data["height"]
|
| 463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
range_x = width + 1.0
|
| 465 |
range_y = height + 1.0
|
|
|
|
| 466 |
norm_x = click_x / IMG_WIDTH_PX
|
| 467 |
norm_y = click_y / IMG_HEIGHT_PX
|
|
|
|
|
|
|
| 468 |
data_x = norm_x * range_x - 0.5
|
| 469 |
data_y = norm_y * range_y - 0.5
|
|
|
|
| 470 |
grid_x = int(round(data_x))
|
| 471 |
grid_y = int(round(data_y))
|
| 472 |
|
|
|
|
| 473 |
gen = NetworkGenerator(width, height)
|
| 474 |
gen.graph = state_data["graph"]
|
| 475 |
|
|
|
|
| 476 |
if 0 <= grid_x <= width and 0 <= grid_y <= height:
|
| 477 |
if gen.graph.has_node((grid_x, grid_y)):
|
|
|
|
| 478 |
success, msg = gen.manual_delete_node(grid_x, grid_y)
|
| 479 |
action = "Deleted"
|
| 480 |
else:
|
|
|
|
| 481 |
success, msg = gen.manual_add_node(grid_x, grid_y)
|
| 482 |
action = "Added"
|
| 483 |
else:
|
| 484 |
+
success, msg, action = False, "Clicked outside.", "Ignored"
|
|
|
|
|
|
|
| 485 |
|
|
|
|
| 486 |
if success:
|
| 487 |
state_data["graph"] = gen.graph
|
| 488 |
img_path = plot_graph_to_image(gen.graph, width, height)
|
|
|
|
| 491 |
else:
|
| 492 |
return gr.update(), f"Error: {msg}", state_data
|
| 493 |
|
|
|
|
| 494 |
def get_preset_dims(preset_mode, topology):
|
| 495 |
if preset_mode == "Custom": return gr.update(interactive=True), gr.update(interactive=True)
|
| 496 |
+
dims = (6, 11) if topology=="linear" and preset_mode=="Medium" else (8,8)
|
| 497 |
if preset_mode == "Small": dims = (4, 4)
|
| 498 |
if preset_mode == "Large": dims = (16, 16) if topology!="linear" else (10, 26)
|
| 499 |
return gr.update(value=dims[0], interactive=False), gr.update(value=dims[1], interactive=False)
|
|
|
|
| 514 |
if variant == "Fixed": t_nodes, t_edges = 0, 0
|
| 515 |
gen = NetworkGenerator(width, height, var_code, topology, void_frac, t_nodes, t_edges)
|
| 516 |
graph = gen.generate()
|
|
|
|
| 517 |
img_path = plot_graph_to_image(graph, width, height)
|
| 518 |
metrics = f"**Nodes:** {len(graph.nodes())} | **Edges:** {len(graph.edges())} | **Density:** {nx.density(graph):.2f}"
|
| 519 |
state_data = { "graph": graph, "width": width, "height": height, "topology": topology }
|
| 520 |
+
return img_path, metrics, state_data, gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)
|
| 521 |
except Exception as e:
|
| 522 |
+
return None, f"Error: {e}", None, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)
|
| 523 |
|
| 524 |
def run_batch_generation(count, topology, width, height, variant, void_frac, t_nodes, t_edges):
|
|
|
|
| 525 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 526 |
dir_name = f"batch_{timestamp}"
|
| 527 |
os.makedirs(dir_name, exist_ok=True)
|
|
|
|
| 540 |
except Exception as e:
|
| 541 |
return None
|
| 542 |
|
| 543 |
+
def save_single_json_action(state_data):
|
| 544 |
+
if not state_data or "graph" not in state_data: return None
|
| 545 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 546 |
+
json_content = generate_full_json_dict(state_data["graph"], loop=1)
|
| 547 |
+
fname = f"single_network_{timestamp}.json"
|
| 548 |
+
with open(fname, 'w') as f:
|
| 549 |
+
json.dump(json_content, f, indent=4)
|
| 550 |
+
return fname
|
| 551 |
+
|
| 552 |
# ==========================================
|
| 553 |
# GRADIO UI
|
| 554 |
# ==========================================
|
|
|
|
| 565 |
with gr.Row():
|
| 566 |
width = gr.Number(8, label="Width", interactive=False)
|
| 567 |
height = gr.Number(8, label="Height", interactive=False)
|
| 568 |
+
|
| 569 |
+
with gr.Group():
|
| 570 |
+
variant = gr.Dropdown(["Fixed", "Custom"], value="Fixed", label="Variant")
|
| 571 |
+
with gr.Row():
|
| 572 |
+
t_nodes = gr.Number(0, label="Target Nodes", interactive=False)
|
| 573 |
+
t_edges = gr.Number(0, label="Target Edges", interactive=False)
|
| 574 |
+
|
| 575 |
void_frac = gr.Slider(0.0, 0.9, 0.35, label="Void Fraction", interactive=False)
|
| 576 |
+
|
|
|
|
|
|
|
|
|
|
| 577 |
gen_btn = gr.Button("Generate", variant="primary")
|
| 578 |
+
with gr.Row():
|
| 579 |
+
save_json_btn = gr.Button("Download JSON", interactive=False)
|
| 580 |
+
json_file = gr.File(label="Saved JSON", visible=False)
|
| 581 |
+
|
| 582 |
with gr.Tab("Batch"):
|
| 583 |
batch_count = gr.Slider(1, 50, 5, step=1, label="Count")
|
| 584 |
batch_btn = gr.Button("Generate Batch ZIP")
|
|
|
|
| 586 |
|
| 587 |
with gr.Column(scale=2):
|
| 588 |
metrics = gr.Markdown("Ready.")
|
|
|
|
|
|
|
| 589 |
plot_img = gr.Image(label="Interactive Graph", interactive=False, height=800, width=800)
|
| 590 |
|
| 591 |
# EVENTS
|
|
|
|
| 600 |
topology.change(update_ui_for_variant, inputs_var, [void_frac, t_nodes, t_edges])
|
| 601 |
|
| 602 |
gen_args = [topology, width, height, variant, void_frac, t_nodes, t_edges]
|
| 603 |
+
gen_btn.click(generate_and_store, gen_args, [plot_img, metrics, state, save_json_btn, json_file])
|
| 604 |
|
|
|
|
| 605 |
plot_img.select(handle_plot_click, [state], [plot_img, metrics, state])
|
| 606 |
|
| 607 |
+
save_json_btn.click(save_single_json_action, [state], [json_file]).then(
|
| 608 |
+
lambda: gr.update(visible=True), None, [json_file]
|
| 609 |
+
)
|
| 610 |
+
|
| 611 |
batch_args = [batch_count, topology, width, height, variant, void_frac, t_nodes, t_edges]
|
| 612 |
batch_btn.click(run_batch_generation, batch_args, [file_out])
|
| 613 |
|