Spaces:
Runtime error
Runtime error
Download src/streamlit_app.py from Sierpinksi-Project/Sierpinski-Project: direct link, hf CLI and curl.
- Browser
- Download file 18.9 kB
-
https://huggingface.co/spaces/Sierpinksi-Project/Sierpinski-Project/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/Sierpinksi-Project/Sierpinski-Project/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/Sierpinksi-Project/Sierpinski-Project/resolve/main/src/streamlit_app.py
18.9 kB
| import io | |
| import json | |
| from typing import Dict, Tuple | |
| from itertools import combinations | |
| import imageio.v2 as imageio | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import streamlit as st | |
| import networkx as nx | |
| from ngf import NGFGenerator, EnergyDistribution | |
| st.set_page_config(page_title="NGF Generator (Bianconi–Rahmede) + GIF", layout="wide") | |
| # ---------- Sidebar Controls ---------- | |
| st.sidebar.title("NGF Controls") | |
| with st.sidebar: | |
| st.markdown("**Network Geometry with Flavor (NGF)**") | |
| d = st.number_input("Dimension d", min_value=1, max_value=3, value=2, step=1) | |
| s = st.selectbox("Flavor s", options=[-1, 0, 1], index=1) | |
| beta = st.number_input("Inverse temperature β", min_value=0.0, value=0.1, step=0.1) | |
| if "total_nodes_exact" not in st.session_state: | |
| st.session_state.total_nodes_exact = 1000 | |
| st.caption("N (exact)") | |
| total_nodes = st.number_input( | |
| "N (exact)", | |
| min_value=2, max_value=50000, value=int(st.session_state.total_nodes_exact), step=1, | |
| label_visibility="collapsed", | |
| help="Exakten Knotenwert eingeben (2–50000)." | |
| ) | |
| st.session_state.total_nodes_exact = int(total_nodes) | |
| seed = st.number_input("Random seed", min_value=0, value=42, step=1) | |
| st.divider() | |
| st.markdown("**Energy distribution g(ε)**") | |
| dist_kind = st.selectbox("Kind", ["uniform_int", "poisson", "custom_list"], index=0) | |
| if dist_kind == "uniform_int": | |
| e_low = st.number_input("ε low (inclusive)", value=0, step=1) | |
| e_high = st.number_input("ε high (exclusive)", value=10, step=1) | |
| dist_params = {"low": int(e_low), "high": int(e_high)} | |
| elif dist_kind == "poisson": | |
| lam = st.number_input("λ (Poisson mean)", value=4.0, step=0.5) | |
| dist_params = {"lam": float(lam)} | |
| else: | |
| vals = st.text_input("Custom integer values (comma-separated)", "0,1,2,3,5,8,13") | |
| try: | |
| arr = [int(x.strip()) for x in vals.split(",") if x.strip()!=""] | |
| except Exception: | |
| arr = [0,1,2,3] | |
| dist_params = {"values": arr} | |
| st.divider() | |
| st.markdown("**Visualization options**") | |
| show_triangles = st.checkbox("Show 2-faces (triangles) as mesh (if d≥2)", value=True) | |
| show_boundary = st.checkbox("Highlight boundary (d-1)-faces", value=False) | |
| size_by_degree = st.checkbox("Scale node size by 1-skeleton degree (Fig. 2 style)", value=True) | |
| st.divider() | |
| do_generate = st.button("Generate NGF", type="primary") | |
| # Animation controls under the main button | |
| fps = st.number_input("Animation FPS", min_value=1, max_value=60, value=12, step=1) | |
| step_N = st.number_input("ΔN pro Frame", min_value=1, max_value=5000, value=20, step=1) | |
| do_anim = st.button("Generate Animation") | |
| def build_ngf(d, s, beta, total_nodes, dist_kind, dist_params, seed): | |
| gen = NGFGenerator( | |
| d=int(d), | |
| s=int(s), | |
| beta=float(beta), | |
| energy_dist=EnergyDistribution(kind=dist_kind, params=dist_params, seed=seed), | |
| seed=int(seed), | |
| ) | |
| gen.run(total_nodes=int(total_nodes)) | |
| return gen | |
| def graph_layout_3d(G: nx.Graph, seed: int = 42) -> Dict[int, Tuple[float, float, float]]: | |
| """Preferred: spring_layout in 3D (requires SciPy). Fallback: random_layout (SciPy-free).""" | |
| try: | |
| import scipy # probe availability | |
| pos = nx.spring_layout(G, dim=3, seed=seed, k=None, iterations=200) | |
| except Exception: | |
| pos = nx.random_layout(G, center=(0.0, 0.0, 0.0), dim=3, seed=seed) | |
| return {int(k): (float(v[0]), float(v[1]), float(v[2])) for k, v in pos.items()} | |
| def plot_ngf_3d(gen: NGFGenerator, show_triangles: bool = True, show_boundary: bool = False, size_by_degree: bool = True): | |
| from itertools import combinations | |
| G = gen.to_networkx() | |
| pos = graph_layout_3d(G, seed=123) | |
| import numpy as np | |
| energies = np.array([gen.node_energy[i] for i in gen.nodes], dtype=float) | |
| deg1 = gen.node_degrees_1skeleton() | |
| degrees = np.array([deg1[i] for i in gen.nodes], dtype=float) | |
| # Node size ∝ degree (robust scaling) | |
| if size_by_degree and len(degrees): | |
| deg_min = degrees.min() | |
| deg_range = float(np.ptp(degrees)) | |
| size_min, size_max = 6.0, 22.0 | |
| if deg_range <= 1e-12: | |
| node_sizes = np.full_like(energies, (size_min + size_max) * 0.5) | |
| else: | |
| node_sizes = size_min + (size_max - size_min) * (degrees - deg_min) / deg_range | |
| else: | |
| node_sizes = np.full_like(energies, 10.0) | |
| xs = [pos[i][0] for i in gen.nodes] | |
| ys = [pos[i][1] for i in gen.nodes] | |
| zs = [pos[i][2] for i in gen.nodes] | |
| import plotly.graph_objects as go | |
| node_trace = go.Scatter3d( | |
| x=xs, y=ys, z=zs, | |
| mode="markers", | |
| marker=dict( | |
| size=node_sizes, | |
| color=energies if len(energies) else [0.0], | |
| cmin=float(energies.min()) if len(energies) else 0.0, | |
| cmax=float(energies.max()) if len(energies) else 1.0, | |
| colorscale=[ | |
| [0.0, "#ff0000"], [0.2, "#ff7f00"], [0.4, "#ffff00"], | |
| [0.6, "#00ff00"], [0.8, "#0000ff"], [1.0, "#8b00ff"], | |
| ], | |
| showscale=True, | |
| colorbar=dict(title="ε"), | |
| opacity=0.9, | |
| ), | |
| text=[f"node {i}<br>ε={gen.node_energy[i]}<br>deg={deg1[i]}" for i in gen.nodes], | |
| hoverinfo="text", | |
| name="nodes", | |
| ) | |
| # Edges | |
| ex, ey, ez = [], [], [] | |
| for u, v in gen.edges: | |
| x0, y0, z0 = pos[u] | |
| x1, y1, z1 = pos[v] | |
| ex += [x0, x1, None] | |
| ey += [y0, y1, None] | |
| ez += [z0, z1, None] | |
| edge_trace = go.Scatter3d( | |
| x=ex, y=ey, z=ez, | |
| mode="lines", | |
| line=dict(width=2.0, color="rgba(90,90,90,0.75)"), | |
| hoverinfo="none", | |
| name="edges", | |
| opacity=0.75, | |
| ) | |
| data = [edge_trace, node_trace] | |
| # Triangles (2-faces) | |
| if show_triangles and gen.d >= 2: | |
| tris_set = set() | |
| if gen.d == 2: | |
| for s in gen.simplexes_d: | |
| if len(s) >= 3: | |
| a, b, c = sorted(list(s))[:3] | |
| tris_set.add((a, b, c)) | |
| else: | |
| for s in gen.simplexes_d: | |
| for (a, b, c) in combinations(s, 3): | |
| tris_set.add(tuple(sorted((a, b, c)))) | |
| if tris_set: | |
| idx_map = {nid: idx for idx, nid in enumerate(gen.nodes)} | |
| I, J, K = [], [], [] | |
| for (a, b, c) in tris_set: | |
| if a in idx_map and b in idx_map and c in idx_map: | |
| I.append(idx_map[a]); J.append(idx_map[b]); K.append(idx_map[c]) | |
| if I: | |
| mesh = go.Mesh3d( | |
| x=xs, y=ys, z=zs, | |
| i=I, j=J, k=K, | |
| opacity=0.18, | |
| name="2-faces (triangles)", | |
| hoverinfo="skip" | |
| ) | |
| data.insert(0, mesh) | |
| if show_boundary and gen.d >= 1: | |
| bx, by, bz = [], [], [] | |
| boundary = gen.get_boundary_faces() | |
| for face in boundary: | |
| nodes = list(face) | |
| for i in range(len(nodes)): | |
| for j in range(i + 1, len(nodes)): | |
| u, v = nodes[i], nodes[j] | |
| x0, y0, z0 = pos[u] | |
| x1, y1, z1 = pos[v] | |
| bx += [x0, x1, None] | |
| by += [y0, y1, None] | |
| bz += [z0, z1, None] | |
| if bx: | |
| boundary_trace = go.Scatter3d( | |
| x=bx, y=by, z=bz, mode="lines", | |
| line=dict(width=4, color="rgba(50,50,50,0.35)"), | |
| opacity=0.3, name="boundary (d-1)-faces", hoverinfo="skip" | |
| ) | |
| data.append(boundary_trace) | |
| fig = go.Figure(data=data) | |
| fig.update_layout( | |
| scene=dict(xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False)), | |
| margin=dict(l=0, r=0, t=30, b=0), | |
| legend=dict(orientation="h"), | |
| height=700, | |
| title=f"NGF d={gen.d}, s={gen.s}, β={gen.beta}, N={gen.N}, V={len(gen.simplexes_d)} (Fig. 2 style)" | |
| ) | |
| return fig | |
| def render_frame_png_matplotlib(gen, pos, uptoN, show_triangles, show_boundary, size_by_degree, | |
| width=1200, height=800, dpi=150): | |
| """Matplotlib-Frame ohne Kaleido, mit skalierter Knotengröße (Grad) relativ zur GIF-Größe.""" | |
| import io | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.colors import LinearSegmentedColormap, Normalize | |
| from mpl_toolkits.mplot3d.art3d import Poly3DCollection | |
| from itertools import combinations | |
| import numpy as np | |
| nodes_sub = [i for i in gen.nodes if i < uptoN] | |
| node_set = set(nodes_sub) | |
| edges_sub = [(u, v) for (u, v) in gen.edges if u in node_set and v in node_set] | |
| energies = np.array([gen.node_energy[i] for i in nodes_sub], dtype=float) | |
| # Grad im Teilgraph | |
| deg = {i: 0 for i in nodes_sub} | |
| for u, v in edges_sub: | |
| deg[u] += 1 | |
| deg[v] += 1 | |
| degrees = np.array([deg[i] for i in nodes_sub], dtype=float) if nodes_sub else np.array([]) | |
| # --- Knoten-Skalierung: Pixel -> Points² (Matplotlib erwartet Fläche in pt²) | |
| min_side = float(min(width, height)) | |
| # sinnvolle Relationen zur Bildgröße (Passe bei Bedarf an) | |
| min_px = max(4.0, 0.008 * min_side) # ~6–10 px bei 800–1200 | |
| max_px = max(10.0, 0.030 * min_side) # ~24–36 px bei 800–1200 | |
| if size_by_degree and degrees.size: | |
| dmin = float(degrees.min()) | |
| drng = float(np.ptp(degrees)) | |
| if drng <= 1e-12: | |
| # alle Grade (fast) gleich -> uniforme Größe | |
| sizes_px = np.full_like(degrees, (min_px + max_px) * 0.5) | |
| else: | |
| dnorm = (degrees - dmin) / drng | |
| sizes_px = min_px + (max_px - min_px) * dnorm | |
| else: | |
| sizes_px = np.full_like(energies, (min_px + max_px) * 0.5) | |
| # Umrechnung Pixel -> Points² | |
| px_to_pt = 72.0 / float(dpi) | |
| sizes_pts2 = (sizes_px * px_to_pt) ** 2 | |
| xs = np.array([pos[i][0] for i in nodes_sub]) if nodes_sub else np.array([]) | |
| ys = np.array([pos[i][1] for i in nodes_sub]) if nodes_sub else np.array([]) | |
| zs = np.array([pos[i][2] for i in nodes_sub]) if nodes_sub else np.array([]) | |
| # Fig.2 Farbskala (Energie) | |
| cmap = LinearSegmentedColormap.from_list( | |
| "rainbow6", ["#ff0000", "#ff7f00", "#ffff00", "#00ff00", "#0000ff", "#8b00ff"] | |
| ) | |
| if energies.size: | |
| norm = Normalize(vmin=float(energies.min()), vmax=float(energies.max())) | |
| colors = cmap(norm(energies)) | |
| else: | |
| from numpy import array | |
| norm = Normalize(vmin=0.0, vmax=1.0) | |
| colors = cmap(norm(np.array([0.0]))) | |
| fig = plt.figure(figsize=(width/100.0, height/100.0), dpi=dpi) | |
| ax = fig.add_subplot(111, projection="3d") | |
| ax.set_axis_off() | |
| # Dreiecks-Mesh (2-faces) | |
| if show_triangles and gen.d >= 2 and len(nodes_sub) >= 3: | |
| tris = [] | |
| if gen.d == 2: | |
| for s in gen.simplexes_d: | |
| if max(s) < uptoN and len(s) >= 3: | |
| a, b, c = sorted(list(s))[:3] | |
| if a in node_set and b in node_set and c in node_set: | |
| tris.append([(pos[a][0], pos[a][1], pos[a][2]), | |
| (pos[b][0], pos[b][1], pos[b][2]), | |
| (pos[c][0], pos[c][1], pos[c][2])]) | |
| else: | |
| for s in gen.simplexes_d: | |
| if max(s) < uptoN: | |
| for (a, b, c) in combinations(s, 3): | |
| if a in node_set and b in node_set and c in node_set: | |
| tris.append([(pos[a][0], pos[a][1], pos[a][2]), | |
| (pos[b][0], pos[b][1], pos[b][2]), | |
| (pos[c][0], pos[c][1], pos[c][2])]) | |
| if tris: | |
| mesh = Poly3DCollection(tris, alpha=0.15, facecolor="#9aa0a6", edgecolor="none") | |
| ax.add_collection3d(mesh) | |
| # Kantenbreite relativ zur Bildgröße | |
| lw_edge = max(1.0, 0.0015 * min_side) | |
| for u, v in edges_sub: | |
| x0, y0, z0 = pos[u]; x1, y1, z1 = pos[v] | |
| ax.plot([x0, x1], [y0, y1], [z0, z1], linewidth=lw_edge, color=(0.35, 0.35, 0.35, 0.8)) | |
| # Randkanten (optional etwas dicker) | |
| if show_boundary and gen.d >= 1: | |
| lw_b = max(1.8, 0.0028 * min_side) | |
| for face, n in gen.face_n.items(): | |
| if n == 0 and all(v in node_set for v in face): | |
| f = list(face) | |
| for i in range(len(f)): | |
| for j in range(i + 1, len(f)): | |
| u, v = f[i], f[j] | |
| x0, y0, z0 = pos[u]; x1, y1, z1 = pos[v] | |
| ax.plot([x0, x1], [y0, y1], [z0, z1], linewidth=lw_b, color=(0.2, 0.2, 0.2, 0.35)) | |
| # Knoten | |
| if xs.size: | |
| ax.scatter(xs, ys, zs, s=sizes_pts2, c=colors, depthshade=True) | |
| # halbwegs gleiche Achsenskalierung | |
| mins = np.array([xs.min(), ys.min(), zs.min()]) | |
| maxs = np.array([xs.max(), ys.max(), zs.max()]) | |
| centers = (mins + maxs) / 2.0 | |
| span = (maxs - mins).max() * 0.55 + 1e-9 | |
| ax.set_xlim(centers[0]-span, centers[0]+span) | |
| ax.set_ylim(centers[1]-span, centers[1]+span) | |
| ax.set_zlim(centers[2]-span, centers[2]+span) | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", pad_inches=0.02) | |
| buf.seek(0) | |
| plt.close(fig) | |
| return buf.getvalue() | |
| # ---------- App Body ---------- | |
| st.title("Network Geometry with Flavor — NGF Generator & 3D Viewer (+ GIF, no-kaleido, HF-ready)") | |
| st.caption("Fig. 2 style: node color ~ energy (red→violet), node size ~ degree; 2-faces optional.") | |
| if do_generate: | |
| with st.spinner("Generating NGF..."): | |
| gen = build_ngf(d, s, beta, total_nodes, dist_kind, dist_params, seed) | |
| t1, t2, t3, t4 = st.tabs(["3D View", "Distributions", "Growth", "Export"]) | |
| with t1: | |
| fig = plot_ngf_3d(gen, show_triangles=show_triangles, show_boundary=show_boundary, size_by_degree=size_by_degree) | |
| st.plotly_chart(fig, use_container_width=True) | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("N (nodes)", f"{gen.N}") | |
| with col2: | |
| st.metric("V (d-simplices)", f"{len(gen.simplexes_d)}") | |
| with col3: | |
| st.metric("Area A (boundary faces)", f"{sum(1 for f,n in gen.face_n.items() if n==0)}") | |
| with col4: | |
| st.metric("Edges (1-skeleton)", f"{len(gen.edges)}") | |
| with t2: | |
| deg1 = gen.node_degrees_1skeleton() | |
| hist_deg = {} | |
| for k in deg1.values(): | |
| hist_deg[k] = hist_deg.get(k, 0) + 1 | |
| xs = np.array(list(hist_deg.keys()), dtype=float) | |
| ys = np.array(list(hist_deg.values()), dtype=float) | |
| fig_deg = go.Figure(); fig_deg.add_bar(x=xs, y=ys) | |
| fig_deg.update_layout(title="1-skeleton degree distribution", xaxis_title="k", yaxis_title="count", height=360) | |
| fig_deg.update_xaxes(type="log"); fig_deg.update_yaxes(type="log") | |
| st.plotly_chart(fig_deg, use_container_width=True) | |
| if gen.d >= 1: | |
| hist_d1 = gen.generalized_degree_distribution(gen.d - 1) | |
| xs = np.array(list(hist_d1.keys()), dtype=float); ys = np.array(list(hist_d1.values()), dtype=float) | |
| fig_d1 = go.Figure(); fig_d1.add_bar(x=xs, y=ys) | |
| fig_d1.update_layout(title=f"Generalized degrees k₍{gen.d},{gen.d-1}₎ of (d-1)-faces", height=360) | |
| fig_d1.update_xaxes(type="log"); fig_d1.update_yaxes(type="log") | |
| st.plotly_chart(fig_d1, use_container_width=True) | |
| hist_nodes = gen.generalized_degree_distribution(0) | |
| xs = np.array(list(hist_nodes.keys()), dtype=float); ys = np.array(list(hist_nodes.values()), dtype=float) | |
| fig_n = go.Figure(); fig_n.add_bar(x=xs, y=ys) | |
| fig_n.update_layout(title=f"Generalized degrees k₍{gen.d},0₎ of nodes", height=360) | |
| fig_n.update_xaxes(type="log"); fig_n.update_yaxes(type="log") | |
| st.plotly_chart(fig_n, use_container_width=True) | |
| if gen.d >= 2: | |
| hist_links = gen.generalized_degree_distribution(1) | |
| xs = np.array(list(hist_links.keys()), dtype=float); ys = np.array(list(hist_links.values()), dtype=float) | |
| fig_l = go.Figure(); fig_l.add_bar(x=xs, y=ys) | |
| fig_l.update_layout(title=f"Generalized degrees k₍{gen.d},1₎ of links", height=360) | |
| fig_l.update_xaxes(type="log"); fig_l.update_yaxes(type="log") | |
| st.plotly_chart(fig_l, use_container_width=True) | |
| with t4: | |
| data = gen.to_json() | |
| st.json({"dimension": data["dimension"], "flavor": data["flavor"], "beta": data["beta"], "N": data["N"], "V": len(gen.simplexes_d)}) | |
| j = json.dumps(data).encode("utf-8") | |
| st.download_button("Download NGF as JSON", j, file_name=f"ngf_d{gen.d}_s{gen.s}_N{gen.N}.json", mime="application/json") | |
| # --- Animation (matplotlib-based, BytesIO download) --- | |
| if do_anim: | |
| with st.spinner("Baue Animation (ohne Kaleido, HF-ready)..."): | |
| gen_anim = build_ngf(d, s, beta, total_nodes, dist_kind, dist_params, seed) | |
| G_final = gen_anim.to_networkx() | |
| pos = graph_layout_3d(G_final, seed=123) | |
| startN = gen_anim.d + 1 | |
| endN = int(total_nodes) | |
| steps = list(range(startN, endN + 1, int(step_N))) | |
| if steps[-1] != endN: | |
| steps.append(endN) | |
| max_frames = 600 | |
| if len(steps) > max_frames: | |
| st.warning(f"Viele Frames ({len(steps)}). Kürze auf {max_frames}. Erhöhe ΔN.") | |
| import numpy as np | |
| keep = np.linspace(0, len(steps)-1, max_frames).astype(int).tolist() | |
| steps = [steps[i] for i in keep] | |
| frames = [] | |
| prog = st.progress(0.0) | |
| for i, upto in enumerate(steps): | |
| png_bytes = render_frame_png_matplotlib(gen_anim, pos, upto, | |
| show_triangles=show_triangles, | |
| show_boundary=show_boundary, | |
| size_by_degree=size_by_degree, | |
| width=1200, height=800, dpi=150) | |
| frames.append(imageio.imread(io.BytesIO(png_bytes))) | |
| prog.progress((i + 1) / len(steps)) | |
| # Write GIF to memory buffer for direct download (no filesystem path needed) | |
| gif_buf = io.BytesIO() | |
| imageio.mimsave(gif_buf, frames, format="GIF", fps=int(fps)) | |
| gif_buf.seek(0) | |
| st.success(f"Animation erstellt: {len(steps)} Frames @ {int(fps)} FPS") | |
| st.download_button("Download GIF", data=gif_buf, file_name="ngf_animation.gif", mime="image/gif") | |