MCP: serve tools from the API process over Streamable HTTP at /mcp

- netx_api/mcp_remote: ops / topology / bizMonitor tools, Bearer auth,
  per-tool RBAC filtering, ?groups= filter, readOnlyHint and group meta.
- Mount the official MCP Streamable HTTP app at /mcp (stateless);
  AuthAuditMiddleware is pure ASGI and skips /mcp.
- Let uvicorn chunk MCP responses; some Windows LSPs rewrite
  Content-Length replies on */mcp* paths into broken chunked framing.
- NETX_MCP_ALLOWED_HOSTS opts into DNS-rebinding checks (off by default
  so LAN and reverse-proxy hosts work; Bearer is the gate).
- Remove the three stdio Python MCP packages; tests moved to /mcp,
  including real-TCP framing regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 21:52:08 +08:00
parent 918ece86c3
commit 52679a7cfc
150 changed files with 2367 additions and 2812 deletions

View file

@ -0,0 +1,826 @@
"""Stage-2 crossing pressure without temp scripts.
Actions used by agents via layoutTopologyView:
- press_hot_edges: rotate non-portal ends of top crossing edges about the other end
- press_crossers: move nodes that participate in crossings against those hot edges
- polish_crossings: straighten → hot_edges → crossers → untangle(portals)
Hot path uses incremental crossing deltas (O(deg·E) per trial) so KND-scale
graphs (~500 links) stay interactive under MCP stdio timeouts.
"""
from __future__ import annotations
import math
from typing import Any
from netx_api.mcp_remote.topology.layout_metrics import (
count_edge_crossings,
crossings_after_node_move,
crossings_involving_node,
crossing_participation_full,
node_footprint,
segments_properly_intersect,
top_crossing_edges,
)
from netx_api.mcp_remote.topology.layout_ops.state import LayoutParams, LayoutState, OpResult
from netx_api.mcp_remote.topology.layout_ops.untangle import untangle_crossings
def park_phantom_nodes(state: LayoutState) -> set[str]:
"""Park region: markers / absurd coords so they do not blow bbox/util."""
moved: set[str] = set()
for nid, (x, y) in list(state.positions.items()):
if str(nid).startswith("region:") or abs(x) > 1e5 or abs(y) > 1e5:
if not math.isfinite(x) or not math.isfinite(y) or abs(x) > 1e5 or abs(y) > 1e5:
state.positions[nid] = (160.0, 160.0)
moved.add(nid)
elif str(nid).startswith("region:"):
# keep finite but park markers to origin corner
if abs(x) > 1e4 or abs(y) > 1e4:
state.positions[nid] = (160.0, 160.0)
moved.add(nid)
return moved
def _overlaps_any(
node: str,
pos: dict[str, tuple[float, float]],
names: dict[str, str],
) -> bool:
x, y = pos[node]
fa = node_footprint(names.get(node, ""))
ax0, ay0, ax1, ay1 = x + fa[0], y + fa[1], x + fa[2], y + fa[3]
for b, (bx, by) in pos.items():
if b == node:
continue
if abs(bx - x) > 90 and abs(by - y) > 70:
continue
fb = node_footprint(names.get(b, ""))
bx0, by0, bx1, by1 = bx + fb[0], by + fb[1], bx + fb[2], by + fb[3]
if ax0 < bx1 and ax1 > bx0 and ay0 < by1 and ay1 > by0:
return True
return False
def _rotate_about(
hub: tuple[float, float], leaf: tuple[float, float], ang: float
) -> tuple[float, float]:
hx, hy = hub
lx, ly = leaf
dx, dy = lx - hx, ly - hy
c, s = math.cos(ang), math.sin(ang)
return (hx + dx * c - dy * s, hy + dx * s + dy * c)
def frozen_portals_from_state(
state: LayoutState, portal_ids: list[str] | None = None
) -> set[str]:
from netx_api.mcp_remote.topology.layout_ops.rigid_units import (
_hub_portals,
frozen_ids_for_protect,
)
if portal_ids:
raw = {str(x) for x in portal_ids if str(x)}
# Explicit dual-unit portals (~20) must stay frozen; only shrink if
# caller passed the inflated multi-membership set.
return _hub_portals(state, raw, cap=32)
return frozen_ids_for_protect(state, "portals")
def _large_graph_budget(n_links: int) -> dict[str, Any]:
"""Shrink search when E is large so MCP stdio does not time out.
Target: full polish_crossings on ~500-link graphs finishes in ~15–25s.
"""
e = max(0, int(n_links))
if e >= 1200:
return {
"hot_top_n": 6,
"hot_max_moves": 10,
"hot_max_sweeps": 1,
"hot_angle_step": math.pi / 6,
"hot_radii": [0.95, 1.2],
"cross_top_n": 4,
"cross_max_moves": 12,
"cross_max_sweeps": 1,
"cross_cand_cap": 48,
"straighten": False,
"skip_dual_full_x": True,
"untangle_rounds": 18,
"untangle_moves": 2,
"untangle_rank_cap": 36,
"untangle_angle_step": 30,
}
if e >= 400:
return {
"hot_top_n": 8,
"hot_max_moves": 14,
"hot_max_sweeps": 2,
"hot_angle_step": math.pi / 8,
"hot_radii": [0.9, 1.15, 1.35],
"cross_top_n": 5,
"cross_max_moves": 16,
"cross_max_sweeps": 2,
"cross_cand_cap": 72,
# straighten_channels is O(channels×modes×crossings); on 1k+ link
# metros it can stall for minutes with no cancel checkpoints.
"straighten": False,
"skip_dual_full_x": e >= 800,
"untangle_rounds": 28,
"untangle_moves": 3,
"untangle_rank_cap": 48,
"untangle_angle_step": 24,
}
if e >= 200:
return {
"hot_top_n": 8,
"hot_max_moves": 14,
"hot_max_sweeps": 2,
"hot_angle_step": math.pi / 9,
"hot_radii": [0.9, 1.1, 1.25],
"cross_top_n": 4,
"cross_max_moves": 14,
"cross_max_sweeps": 2,
"cross_cand_cap": 80,
"straighten": False,
"skip_dual_full_x": False,
"untangle_rounds": 24,
"untangle_moves": 3,
"untangle_rank_cap": 60,
"untangle_angle_step": 20,
}
return {
"hot_top_n": 10,
"hot_max_moves": 24,
"hot_max_sweeps": 4,
"hot_angle_step": math.pi / 12,
"hot_radii": [0.85, 1.0, 1.15, 1.35],
"cross_top_n": 6,
"cross_max_moves": 40,
"cross_max_sweeps": 6,
"cross_cand_cap": 220,
# Off by default: straighten flattens dual-unit petal eyes into H-chains.
"straighten": False,
"skip_dual_full_x": False,
"untangle_rounds": 120,
"untangle_moves": 5,
"untangle_rank_cap": 140,
"untangle_angle_step": 15,
}
def _has_petal_dual_eye(state: LayoutState) -> bool:
"""True when a multi-corridor dual-unit covers a large share of the canvas."""
try:
from netx_api.mcp_remote.topology.layout_ops.dual_units import (
classify_dual_unit,
find_dual_portal_units,
)
except Exception:
return False
units = find_dual_portal_units(state, max_units=24)
if not units:
return False
n = max(1, len(state.positions) or len(state.names) or len(state.adj))
for u in units:
if classify_dual_unit(u) != "petal":
continue
if len(u.member_ids()) >= max(8, int(0.25 * n)):
return True
return False
def press_hot_edges(
state: LayoutState,
params: LayoutParams | None = None,
*,
portal_ids: list[str] | None = None,
top_n: int | None = None,
max_moves: int | None = None,
max_sweeps: int | None = None,
) -> OpResult:
"""Rotate non-portal ends of top crossing edges about the other endpoint."""
from netx_api.mcp_remote.topology.layout_jobs import raise_if_cancelled, touch_heartbeat
del params
st = state.copy()
park_phantom_nodes(st)
frozen = frozen_portals_from_state(st, portal_ids)
pos = dict(st.positions)
names = st.names
links = st.links
adj = st.adj
budget = _large_graph_budget(len(links))
# Cap overrides — giant metros must not explode search via params.top_n.
top_n = min(
int(top_n if top_n is not None else budget["hot_top_n"]),
int(budget["hot_top_n"]) + (0 if len(links) >= 800 else 4),
)
max_moves = min(
int(max_moves if max_moves is not None else budget["hot_max_moves"]),
int(budget["hot_max_moves"]) + (0 if len(links) >= 800 else 6),
)
max_sweeps = min(
int(max_sweeps if max_sweeps is not None else budget["hot_max_sweeps"]),
int(budget["hot_max_sweeps"]),
)
angle_step = float(budget["hot_angle_step"])
radii_scale = list(budget["hot_radii"])
angles = [i * angle_step for i in range(1, max(2, int(round(2 * math.pi / angle_step))))]
skip_dual = bool(budget.get("skip_dual_full_x"))
touch_heartbeat()
start = count_edge_crossings(pos, links)
cur = start
moved: set[str] = set()
accepted = 0
for _sweep in range(max(1, int(max_sweeps))):
raise_if_cancelled()
touch_heartbeat()
_n, _nh, edge_hit = crossing_participation_full(pos, links)
touch_heartbeat()
tops = top_crossing_edges(
pos, links, names=names, top_n=top_n, edge_participation=edge_hit
)
sweep_moves = 0
for ei, edge in enumerate(tops):
if ei % 2 == 0:
raise_if_cancelled()
touch_heartbeat()
a, b = str(edge["a_node_id"]), str(edge["b_node_id"])
if a not in pos or b not in pos:
continue
cand_pairs: list[tuple[str, str]] = []
if a not in frozen:
cand_pairs.append((a, b))
if b not in frozen:
cand_pairs.append((b, a))
if not cand_pairs:
continue
cand_pairs.sort(
key=lambda pair: (
0 if pair[1] in frozen else 1,
len(adj.get(pair[0], ())),
)
)
leaf, hub = cand_pairs[0]
hub_xy, leaf_xy = pos[hub], pos[leaf]
r0 = math.hypot(leaf_xy[0] - hub_xy[0], leaf_xy[1] - hub_xy[1]) or 200.0
# Cap orbit radius so long spokes cannot fling leaves across a giant bbox.
# Giant metros still need a higher reel target than 900 (bridges ≫5k).
r_cap = 2200.0 if len(links) >= 400 else 1400.0
r_use = min(r0, r_cap)
max_disp = 1100.0 if len(links) >= 400 else 1100.0
# Inward reel on long metro bridges needs a larger displacement budget
# (otherwise max_disp rejects the only moves that cut crossings).
if r0 > max_disp * 1.5:
max_disp = max(max_disp, min(r0 * 0.55, r0 - 0.35 * r_use))
local_before = crossings_involving_node(leaf, pos, links, adj)
best: tuple[int, tuple[float, float]] | None = None
best_dual: (
tuple[int, tuple[float, float], tuple[float, float]] | None
) = None
def _try_cand(cand: tuple[float, float], disp_cap: float | None = None) -> None:
nonlocal best
cap = max_disp if disp_cap is None else disp_cap
if math.hypot(cand[0] - leaf_xy[0], cand[1] - leaf_xy[1]) > cap:
return
trial_pos = {**pos, leaf: cand}
if _overlaps_any(leaf, trial_pos, names):
return
c1 = crossings_after_node_move(
pos,
links,
adj,
leaf,
cand,
current_total=cur,
local_before=local_before,
)
if c1 < cur and (best is None or c1 < best[0]):
best = (c1, cand)
# Reel-in along the spoke first (shorten without hunting angles).
# Pulling straight toward the hub cannot fling the leaf across the
# bbox, so allow it to travel all the way to the r_use target;
# max_disp alone stops short (e.g. r0=8000 → cap 4400 < 5800 needed).
if r0 > r_use + 50.0:
reel_cap = max(max_disp, r0 - r_use)
ux = (leaf_xy[0] - hub_xy[0]) / r0
uy = (leaf_xy[1] - hub_xy[1]) / r0
for rt in (
r_use,
r_use * 1.15,
max(220.0, r0 * 0.25),
max(220.0, r0 * 0.4),
max(220.0, r0 * 0.55),
max(220.0, r0 * 0.7),
):
rt = min(rt, r0 - 1.0)
if rt < 120.0:
continue
_try_cand((hub_xy[0] + ux * rt, hub_xy[1] + uy * rt), reel_cap)
# Both ends walk toward the midpoint (metro bridges often need this).
# Skip on giant E: each trial is a full O(E²) count and stalls MCP.
if (not skip_dual) and r0 > 1500.0 and hub not in frozen:
mx = 0.5 * (hub_xy[0] + leaf_xy[0])
my = 0.5 * (hub_xy[1] + leaf_xy[1])
ux = (leaf_xy[0] - hub_xy[0]) / r0
uy = (leaf_xy[1] - hub_xy[1]) / r0
px, py = -uy, ux
dual_plans: list[tuple[tuple[float, float], tuple[float, float]]] = []
for t in (0.15, 0.25, 0.35, 0.45, 0.55):
dual_plans.append(
(
(
leaf_xy[0] + t * (mx - leaf_xy[0]),
leaf_xy[1] + t * (my - leaf_xy[1]),
),
(
hub_xy[0] + t * (mx - hub_xy[0]),
hub_xy[1] + t * (my - hub_xy[1]),
),
)
)
# Asymmetric: reel the free leaf harder than the hub.
for tl, th in ((0.45, 0.15), (0.6, 0.2), (0.7, 0.25)):
dual_plans.append(
(
(
leaf_xy[0] + tl * (mx - leaf_xy[0]),
leaf_xy[1] + tl * (my - leaf_xy[1]),
),
(
hub_xy[0] + th * (mx - hub_xy[0]),
hub_xy[1] + th * (my - hub_xy[1]),
),
)
)
for s in (-900.0, -520.0, -280.0, 280.0, 520.0, 900.0):
dual_plans.append(
(
(leaf_xy[0] + px * s, leaf_xy[1] + py * s),
(hub_xy[0] + px * s, hub_xy[1] + py * s),
)
)
for leaf_c, hub_c in dual_plans:
trial = {**pos, leaf: leaf_c, hub: hub_c}
if _overlaps_any(leaf, trial, names) or _overlaps_any(
hub, trial, names
):
continue
c1 = count_edge_crossings(trial, links)
if c1 < cur and (best_dual is None or c1 < best_dual[0]):
best_dual = (c1, leaf_c, hub_c)
for ang in angles:
for rs in radii_scale:
rx, ry = _rotate_about(hub_xy, leaf_xy, ang)
dx, dy = rx - hub_xy[0], ry - hub_xy[1]
L = math.hypot(dx, dy) or 1.0
cand = (
hub_xy[0] + dx / L * r_use * min(rs, 1.25),
hub_xy[1] + dy / L * r_use * min(rs, 1.25),
)
_try_cand(cand)
if best is None:
hx, hy = hub_xy
lx, ly = leaf_xy
dx, dy = lx - hx, ly - hy
L = math.hypot(dx, dy) or 1.0
px, py = -dy / L, dx / L
for s in (-720.0, -480.0, -320.0, -200.0, -120.0, 120.0, 200.0, 320.0, 480.0, 720.0):
_try_cand((lx + px * s, ly + py * s))
# Prefer dual mid-walk when it beats single-end moves.
if best_dual is not None and (
best is None or best_dual[0] < best[0]
):
pos[leaf] = best_dual[1]
pos[hub] = best_dual[2]
cur = best_dual[0]
moved.add(leaf)
moved.add(hub)
accepted += 1
sweep_moves += 1
if accepted >= max_moves:
break
continue
if best is None:
continue
pos[leaf] = best[1]
cur = best[0]
moved.add(leaf)
accepted += 1
sweep_moves += 1
if accepted >= max_moves:
break
if sweep_moves == 0 or accepted >= max_moves:
break
st.positions = pos
st.meta = dict(st.meta or {})
st.meta["press_hot_edges"] = {
"start_crossings": start,
"end_crossings": cur,
"accepted_moves": accepted,
"frozen_n": len(frozen),
"budget": {"top_n": top_n, "max_moves": max_moves, "max_sweeps": max_sweeps},
}
return OpResult(
state=st,
moved=moved,
op="press_hot_edges",
params=st.meta["press_hot_edges"],
note=f"press_hot_edges {start}->{cur} moves={accepted}",
)
def _edges_crossing(
pos: dict[str, tuple[float, float]],
links: list[tuple[str, str]],
a: str,
b: str,
) -> list[tuple[str, str]]:
if a not in pos or b not in pos:
return []
p1, p2 = pos[a], pos[b]
out: list[tuple[str, str]] = []
for c, d in links:
if len({a, b, c, d}) < 4 or c not in pos or d not in pos:
continue
if segments_properly_intersect(p1, p2, pos[c], pos[d]):
out.append((c, d))
return out
def _try_move_node(
pos: dict[str, tuple[float, float]],
names: dict[str, str],
links: list[tuple[str, str]],
adj: dict[str, set[str]],
node: str,
*,
jump: float,
cur: int,
cand_cap: int = 220,
) -> tuple[int, tuple[float, float]] | None:
x0, y0 = pos[node]
before_local = crossings_involving_node(node, pos, links, adj)
if before_local <= 0:
return None
ang_step = 30 if cand_cap <= 60 else (20 if cand_cap <= 100 else 10)
# Keep jumps local — long radii explode util on metro canvases.
# Large graphs still need mid-range radii to clear hot-edge crossers.
radii = (
(120, 220, 360, 520)
if cand_cap <= 80
else (80, 140, 220, 320, 450, 600, 800, 1100)
)
cands: list[tuple[float, float]] = []
for ang in range(0, 360, ang_step):
for r in radii:
if r > jump:
continue
rad = math.radians(ang)
cands.append((x0 + r * math.cos(rad), y0 + r * math.sin(rad)))
nbs = [pos[v] for v in adj.get(node, ()) if v in pos]
if nbs:
cx = sum(p[0] for p in nbs) / len(nbs)
cy = sum(p[1] for p in nbs) / len(nbs)
dx, dy = x0 - cx, y0 - cy
L = math.hypot(dx, dy) or 1.0
ux, uy = dx / L, dy / L
px, py = -uy, ux
for s in (-800.0, -500.0, -350.0, -200.0, 200.0, 350.0, 500.0, 800.0):
cands.append((x0 + ux * s, y0 + uy * s))
cands.append((x0 + px * s, y0 + py * s))
if len(cands) > cand_cap:
cands = cands[:: max(1, len(cands) // cand_cap)]
best: tuple[int, tuple[float, float]] | None = None
for cand in cands:
if math.hypot(cand[0] - x0, cand[1] - y0) > jump + 1:
continue
trial_pos = {**pos, node: cand}
if _overlaps_any(node, trial_pos, names):
continue
after_local = crossings_involving_node(node, trial_pos, links, adj)
if after_local >= before_local:
continue
c1 = cur - before_local + after_local
if c1 < cur and (best is None or c1 < best[0]):
best = (c1, cand)
return best
def press_crossers(
state: LayoutState,
params: LayoutParams | None = None,
*,
portal_ids: list[str] | None = None,
top_n: int | None = None,
max_moves: int | None = None,
max_sweeps: int | None = None,
) -> OpResult:
"""Move non-portal nodes involved in crossings against top hot edges."""
del params
st = state.copy()
park_phantom_nodes(st)
frozen = frozen_portals_from_state(st, portal_ids)
pos = dict(st.positions)
names = st.names
links = st.links
adj = st.adj
budget = _large_graph_budget(len(links))
top_n = int(top_n if top_n is not None else budget["cross_top_n"])
max_moves = int(max_moves if max_moves is not None else budget["cross_max_moves"])
max_sweeps = int(max_sweeps if max_sweeps is not None else budget["cross_max_sweeps"])
cand_cap = int(budget["cross_cand_cap"])
start = count_edge_crossings(pos, links)
cur = start
moved: set[str] = set()
accepted = 0
for _sweep in range(max(1, int(max_sweeps))):
_n, _nh, edge_hit = crossing_participation_full(pos, links)
tops = top_crossing_edges(
pos, links, names=names, top_n=top_n, edge_participation=edge_hit
)
scores: dict[str, int] = {}
for he in tops:
a, b = str(he["a_node_id"]), str(he["b_node_id"])
for c, d in _edges_crossing(pos, links, a, b):
for nid in (c, d, a, b):
if nid in frozen or nid not in pos:
continue
deg = len(adj.get(nid, ()))
if deg >= 16:
continue
scores[nid] = scores.get(nid, 0) + int(he["crossing_hits"]) + max(
0, 10 - deg
)
ranked = sorted(scores, key=lambda n: (-scores[n], len(adj.get(n, ())), n))
node_cap = 16 if cand_cap <= 60 else (24 if cand_cap <= 100 else 40)
improved = False
for node in ranked[:node_cap]:
jump = 1400.0 if len(adj.get(node, ())) <= 4 else 1000.0
best = _try_move_node(
pos, names, links, adj, node, jump=jump, cur=cur, cand_cap=cand_cap
)
if best is None:
continue
pos[node] = best[1]
cur = best[0]
moved.add(node)
accepted += 1
improved = True
if accepted >= max_moves:
break
if not improved or accepted >= max_moves:
break
st.positions = pos
st.meta = dict(st.meta or {})
st.meta["press_crossers"] = {
"start_crossings": start,
"end_crossings": cur,
"accepted_moves": accepted,
"frozen_n": len(frozen),
"budget": {"top_n": top_n, "max_moves": max_moves, "max_sweeps": max_sweeps},
}
return OpResult(
state=st,
moved=moved,
op="press_crossers",
params=st.meta["press_crossers"],
note=f"press_crossers {start}->{cur} moves={accepted}",
)
def polish_crossings(
state: LayoutState,
params: LayoutParams | None = None,
*,
portal_ids: list[str] | None = None,
straighten: bool | None = None,
max_degree: int = 9,
untangle_rounds: int | None = None,
top_n: int | None = None,
max_moves: int | None = None,
max_sweeps: int | None = None,
preserve_dual_eye: bool | None = None,
) -> OpResult:
"""Pipeline: park phantoms → (optional straighten) → hot → crossers → untangle.
Petal dual-unit eyes: straighten stays off unless ``straighten=true`` —
channel flattening destroys parallel-lane eye geometry.
"""
from netx_api.mcp_remote.topology.layout_jobs import raise_if_cancelled, report_progress
params = params or LayoutParams()
st = state.copy()
park_phantom_nodes(st)
budget = _large_graph_budget(len(st.links))
preserve = True if preserve_dual_eye is None else bool(preserve_dual_eye)
petal_eye = False
eye_portals: list[str] = []
if preserve:
try:
from netx_api.mcp_remote.topology.layout_ops.dual_units import (
classify_dual_unit,
find_dual_portal_units,
)
units = find_dual_portal_units(st, max_units=24)
n = max(1, len(st.positions) or len(st.names) or len(st.adj))
for u in units:
if classify_dual_unit(u) != "petal":
continue
if len(u.member_ids()) >= max(8, int(0.25 * n)):
petal_eye = True
eye_portals = [u.portal_a, u.portal_b]
break
except Exception:
petal_eye = _has_petal_dual_eye(st)
if portal_ids is None and eye_portals:
portal_ids = eye_portals
# Giant graphs: never allow straighten even if caller asks (stalls for minutes).
if len(st.links) >= 800:
do_straighten = False
elif petal_eye and straighten is not True:
do_straighten = False
else:
do_straighten = (
bool(budget["straighten"]) if straighten is None else bool(straighten)
)
untangle_rounds = int(
untangle_rounds if untangle_rounds is not None else budget["untangle_rounds"]
)
if len(st.links) >= 800:
untangle_rounds = min(untangle_rounds, int(budget["untangle_rounds"]))
top_n = (
min(int(top_n), int(budget["hot_top_n"]))
if top_n is not None
else top_n
)
max_moves = (
min(int(max_moves), int(budget["hot_max_moves"]))
if max_moves is not None
else max_moves
)
report_progress(
"polish_start",
pct=42.0,
message=f"links={len(st.links)} rounds={untangle_rounds}",
links=len(st.links),
untangle_rounds=untangle_rounds,
)
start = count_edge_crossings(st.positions, st.links)
trace: list[dict[str, Any]] = []
moved: set[str] = set()
if do_straighten:
from netx_api.mcp_remote.topology.layout_ops.channels import straighten_channels_greedy
raise_if_cancelled()
report_progress("polish_straighten", pct=48.0, message=f"x0={start}")
op = straighten_channels_greedy(st, params)
st = op.state
moved |= op.moved
trace.append({"op": "straighten_channels", "note": op.note, **(op.params or {})})
raise_if_cancelled()
report_progress("polish_hot_edges", pct=55.0, message="press_hot_edges")
op_h = press_hot_edges(
st,
params,
portal_ids=portal_ids,
top_n=top_n,
max_moves=max_moves,
max_sweeps=max_sweeps,
)
st = op_h.state
moved |= op_h.moved
trace.append({"op": "press_hot_edges", "note": op_h.note, **(op_h.params or {})})
raise_if_cancelled()
report_progress("polish_crossers", pct=62.0, message="press_crossers")
op_c = press_crossers(
st,
params,
portal_ids=portal_ids,
top_n=top_n,
max_moves=max_moves,
max_sweeps=max_sweeps,
)
st = op_c.state
moved |= op_c.moved
trace.append({"op": "press_crossers", "note": op_c.note, **(op_c.params or {})})
focus: list[str] = []
_n, node_hit, edge_hit = crossing_participation_full(st.positions, st.links)
tops_e = top_crossing_edges(
st.positions, st.links, names=st.names, top_n=5, edge_participation=edge_hit
)
for row in tops_e:
focus.append(str(row["a_node_id"]))
focus.append(str(row["b_node_id"]))
ranked_nodes = sorted(node_hit.items(), key=lambda kv: -kv[1])[:5]
focus.extend(nid for nid, _ in ranked_nodes)
focus = list(dict.fromkeys(focus))
raise_if_cancelled()
report_progress(
"polish_untangle",
pct=70.0,
message=f"untangle rounds={untangle_rounds} focus={len(focus)}",
untangle_rounds=untangle_rounds,
focus_n=len(focus),
)
op_u = untangle_crossings(
st,
params,
protect_rigid="portals",
focus_ids=focus,
max_rounds=untangle_rounds,
max_degree=max_degree,
target_crossings=40,
max_jump=1000.0,
moves_per_round=int(budget["untangle_moves"]),
frozen_ids=frozen_portals_from_state(st, portal_ids) if portal_ids else None,
rank_cap=int(budget["untangle_rank_cap"]),
angle_step=int(budget["untangle_angle_step"]),
refresh_every=3 if len(st.links) >= 200 else 1,
)
st = op_u.state
moved |= op_u.moved
trace.append({"op": "untangle", "note": op_u.note, **(op_u.params or {})})
end = count_edge_crossings(st.positions, st.links)
report_progress(
"polish_done",
pct=74.0,
message=f"crossings {start}->{end}",
crossings_before=start,
crossings_after=end,
)
st.meta = dict(st.meta or {})
st.meta["polish_crossings"] = {
"start_crossings": start,
"end_crossings": end,
"trace": trace,
"focus_n": len(focus),
"budget": budget,
"straighten": do_straighten,
"preserve_dual_eye": preserve,
"petal_eye_detected": petal_eye,
"untangle_rounds": untangle_rounds,
}
return OpResult(
state=st,
moved=moved,
op="polish_crossings",
params=st.meta["polish_crossings"],
note=f"polish_crossings {start}->{end}",
)
def press_params_from_overrides(overrides: dict[str, Any] | None) -> dict[str, Any]:
o = overrides or {}
out: dict[str, Any] = {}
for key, cast in (
("top_n", int),
("max_moves", int),
("max_sweeps", int),
("max_degree", int),
("untangle_rounds", int),
):
if key in o and o[key] is not None:
try:
out[key] = cast(o[key])
except (TypeError, ValueError):
pass
if "straighten" in o:
v = o["straighten"]
out["straighten"] = (
v
if isinstance(v, bool)
else str(v).strip().lower() in {"1", "true", "yes", "on"}
)
if "preserve_dual_eye" in o:
v = o["preserve_dual_eye"]
out["preserve_dual_eye"] = (
v
if isinstance(v, bool)
else str(v).strip().lower() in {"1", "true", "yes", "on"}
)
if "portal_ids" in o and isinstance(o["portal_ids"], list):
out["portal_ids"] = [str(x) for x in o["portal_ids"] if str(x)]
if "source_view_ids" in o and isinstance(o["source_view_ids"], list):
out["source_view_ids"] = [str(x) for x in o["source_view_ids"] if str(x)]
return out