mirror of
https://github.com/hansjone/netx.git
synced 2026-10-17 09:43:58 +08:00
- 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>
781 lines
26 KiB
Python
781 lines
26 KiB
Python
"""Inward densify sweep: top-3 single-node pulls + corridor_cap scan.
|
||
|
||
Agent workflow: preview → pick rank 1..3 → apply; round=true auto-applies #1
|
||
when global crossings do not rise and stretch falls; phase=corridor / corridor=true
|
||
sweeps shrink_long_corridors params for util gain.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Any
|
||
|
||
from netx_api.mcp_remote.topology.layout_metrics import (
|
||
count_edge_crossings,
|
||
crossings_involving_node,
|
||
)
|
||
from netx_api.mcp_remote.topology.layout_ops.hotspots import overlapping_nodes
|
||
from netx_api.mcp_remote.topology.layout_ops.orbit_sweep import (
|
||
_diversify_top,
|
||
_eval_candidate,
|
||
_incident_stretch,
|
||
)
|
||
from netx_api.mcp_remote.topology.layout_ops.rigid_units import (
|
||
frozen_ids_for_protect,
|
||
groups_from_compose_meta,
|
||
shrink_long_corridors,
|
||
)
|
||
from netx_api.mcp_remote.topology.layout_ops.score import score_state
|
||
from netx_api.mcp_remote.topology.layout_ops.state import LayoutParams, LayoutState, OpResult
|
||
|
||
_MAX_PULL = 900.0
|
||
_MAX_PULL_CAP = 2200.0
|
||
_PULL_FRACS = (0.12, 0.22, 0.35, 0.5, 0.7)
|
||
_ANGLE_JITTERS = (-30.0, -15.0, 0.0, 15.0, 30.0)
|
||
_DEFAULT_CORRIDOR_CAPS = (1200.0, 1600.0, 2200.0, 3200.0)
|
||
_DEFAULT_PULLS = (0.35, 0.5, 0.65)
|
||
|
||
|
||
def _protect_is_off(protect_rigid: bool | str) -> bool:
|
||
return protect_rigid in (False, "false", "off", "none", "0")
|
||
|
||
|
||
def _resolve_frozen(
|
||
st: LayoutState,
|
||
protect_rigid: bool | str,
|
||
frozen_ids: set[str] | None,
|
||
) -> set[str]:
|
||
"""Portal/rigid freeze only when protect is on; off ignores portal_ids inject."""
|
||
if _protect_is_off(protect_rigid):
|
||
return set()
|
||
frozen: set[str] = set(frozen_ids or ())
|
||
if not frozen:
|
||
frozen = frozen_ids_for_protect(st, protect_rigid)
|
||
return frozen
|
||
|
||
|
||
def _groups_from_state(state: LayoutState) -> list[dict[str, Any]]:
|
||
return groups_from_compose_meta((state.meta or {}).get("compose_views"))
|
||
|
||
|
||
def _membership_maps(
|
||
groups: list[dict[str, Any]],
|
||
valid: set[str],
|
||
) -> tuple[dict[str, list[str]], set[str], dict[str, tuple[float, float]]]:
|
||
"""Return node→group_keys, shared portals, group_key→exclusive centroid xy."""
|
||
counts: dict[str, int] = {}
|
||
g_members: dict[str, list[str]] = {}
|
||
for g in groups:
|
||
key = str(g.get("key") or "")
|
||
members = [str(n) for n in (g.get("node_ids") or []) if str(n) in valid]
|
||
if len(members) < 2:
|
||
continue
|
||
g_members[key] = members
|
||
for n in members:
|
||
counts[n] = counts.get(n, 0) + 1
|
||
shared = {n for n, c in counts.items() if c > 1}
|
||
# Need positions later — centroids filled by caller.
|
||
return (
|
||
{n: [k for k, ms in g_members.items() if n in ms] for n in counts},
|
||
shared,
|
||
{},
|
||
)
|
||
|
||
|
||
def _exclusive_centroids(
|
||
groups: list[dict[str, Any]],
|
||
pos: dict[str, tuple[float, float]],
|
||
shared: set[str],
|
||
) -> dict[str, tuple[float, float]]:
|
||
out: dict[str, tuple[float, float]] = {}
|
||
for g in groups:
|
||
key = str(g.get("key") or "")
|
||
members = [str(n) for n in (g.get("node_ids") or []) if str(n) in pos]
|
||
exclusive = [n for n in members if n not in shared]
|
||
use = exclusive if len(exclusive) >= 1 else members
|
||
if not use:
|
||
continue
|
||
cx = sum(pos[n][0] for n in use) / len(use)
|
||
cy = sum(pos[n][1] for n in use) / len(use)
|
||
out[key] = (cx, cy)
|
||
return out
|
||
|
||
|
||
def _anchor_for_node(
|
||
nid: str,
|
||
pos: dict[str, tuple[float, float]],
|
||
adj: dict[str, set[str]],
|
||
*,
|
||
node_groups: dict[str, list[str]],
|
||
centroids: dict[str, tuple[float, float]],
|
||
global_cx: float,
|
||
global_cy: float,
|
||
) -> tuple[float, float, str]:
|
||
nbs = [pos[v] for v in adj.get(nid, ()) if v in pos]
|
||
if nbs:
|
||
ax = sum(p[0] for p in nbs) / len(nbs)
|
||
ay = sum(p[1] for p in nbs) / len(nbs)
|
||
return ax, ay, "neighbors"
|
||
for gk in node_groups.get(nid, ()):
|
||
if gk in centroids:
|
||
cx, cy = centroids[gk]
|
||
return cx, cy, "unit"
|
||
return global_cx, global_cy, "global"
|
||
|
||
|
||
def _inward_samples(
|
||
x0: float,
|
||
y0: float,
|
||
ax: float,
|
||
ay: float,
|
||
*,
|
||
max_pull: float,
|
||
) -> list[tuple[float, float, float, float]]:
|
||
"""Samples on ray toward anchor; never past the anchor; capped by max_pull."""
|
||
dx, dy = ax - x0, ay - y0
|
||
dist = math.hypot(dx, dy)
|
||
if dist < 8.0:
|
||
return []
|
||
ux, uy = dx / dist, dy / dist
|
||
px, py = -uy, ux
|
||
out: list[tuple[float, float, float, float]] = []
|
||
base_ang = math.degrees(math.atan2(uy, ux)) % 360.0
|
||
for t in _PULL_FRACS:
|
||
pull = min(dist * t, max_pull)
|
||
if pull < 12.0:
|
||
continue
|
||
# Stay short of the anchor (leave 4px slack).
|
||
pull = min(pull, dist - 4.0)
|
||
if pull < 12.0:
|
||
continue
|
||
for dang in _ANGLE_JITTERS:
|
||
if dang == 0.0:
|
||
nx = x0 + ux * pull
|
||
ny = y0 + uy * pull
|
||
ang = base_ang
|
||
else:
|
||
rad = math.radians(base_ang + dang)
|
||
c, s = math.cos(rad), math.sin(rad)
|
||
nx = x0 + c * pull
|
||
ny = y0 + s * pull
|
||
# Reject if farther from anchor than start.
|
||
if math.hypot(nx - ax, ny - ay) >= dist - 1e-6:
|
||
continue
|
||
ang = (base_ang + dang) % 360.0
|
||
r = math.hypot(nx - x0, ny - y0)
|
||
if r < 12.0 or r > max_pull + 1:
|
||
continue
|
||
out.append((nx, ny, r, ang))
|
||
# Mild perpendicular nudges at mid pull (still closer to anchor).
|
||
mid = min(dist * 0.35, max_pull)
|
||
if mid >= 20.0:
|
||
for sign in (-1.0, 1.0):
|
||
nx = x0 + ux * mid + px * sign * mid * 0.25
|
||
ny = y0 + uy * mid + py * sign * mid * 0.25
|
||
if math.hypot(nx - ax, ny - ay) >= dist - 1e-6:
|
||
continue
|
||
r = math.hypot(nx - x0, ny - y0)
|
||
if 12.0 <= r <= max_pull + 1:
|
||
ang = math.degrees(math.atan2(ny - y0, nx - x0)) % 360.0
|
||
out.append((nx, ny, r, ang))
|
||
return out
|
||
|
||
|
||
def _densify_score_key(c: dict[str, Any]) -> tuple:
|
||
# Crossings first; then shorter incident stretch; prefer larger inward pull.
|
||
return (
|
||
int(c["crossings"]["global"]),
|
||
int(c["crossings"]["incident"]),
|
||
float(c.get("stretch") or 1.0),
|
||
-float(c.get("r") or 0.0),
|
||
)
|
||
|
||
|
||
def densify_sweep_node(
|
||
state: LayoutState,
|
||
node_id: str,
|
||
*,
|
||
params: LayoutParams | None = None,
|
||
max_pull: float | None = None,
|
||
nn_floor: float = 60.0,
|
||
min_angle_sep: float = 35.0,
|
||
cand_cap: int = 180,
|
||
protect_rigid: bool | str = "off",
|
||
frozen_ids: set[str] | None = None,
|
||
top_k: int = 3,
|
||
groups: list[dict[str, Any]] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Inward pull candidates for one node; return diversified top-k.
|
||
|
||
Default ``protect_rigid=off`` so multi-round densify may move portals;
|
||
other layout actions keep portal freeze. Pass ``protect_rigid=portals``
|
||
to opt back into rigid protection.
|
||
"""
|
||
params = params or LayoutParams()
|
||
st = state
|
||
nid = str(node_id).strip()
|
||
pos = dict(st.positions)
|
||
names = dict(st.names)
|
||
links = list(st.links)
|
||
adj = {n: set(st.adj.get(n, ())) for n in pos}
|
||
if nid not in pos:
|
||
return {"ok": False, "error": "node_not_on_view", "node_id": nid}
|
||
|
||
frozen = _resolve_frozen(st, protect_rigid, frozen_ids)
|
||
if nid in frozen:
|
||
return {
|
||
"ok": False,
|
||
"error": "frozen",
|
||
"node_id": nid,
|
||
"hint": "portal/rigid frozen; protect_rigid=off or pick a corridor node",
|
||
}
|
||
|
||
pull = float(max_pull if max_pull is not None else _MAX_PULL)
|
||
pull = max(80.0, min(pull, _MAX_PULL_CAP))
|
||
nbs_cap = pull * 2.5
|
||
target_nn = float(getattr(params, "target_nn", 155.0) or 155.0)
|
||
|
||
groups = groups if groups is not None else _groups_from_state(st)
|
||
valid = set(pos)
|
||
node_groups, shared, _ = _membership_maps(groups, valid)
|
||
# With groups + protect on: shared portals stay frozen.
|
||
if groups and nid in shared and not _protect_is_off(protect_rigid):
|
||
return {
|
||
"ok": False,
|
||
"error": "shared_portal",
|
||
"node_id": nid,
|
||
"hint": "shared portals frozen; densify defaults protect_rigid=off",
|
||
}
|
||
|
||
centroids = _exclusive_centroids(groups, pos, shared)
|
||
gcx = sum(p[0] for p in pos.values()) / max(len(pos), 1)
|
||
gcy = sum(p[1] for p in pos.values()) / max(len(pos), 1)
|
||
ax, ay, anchor_kind = _anchor_for_node(
|
||
nid,
|
||
pos,
|
||
adj,
|
||
node_groups=node_groups,
|
||
centroids=centroids,
|
||
global_cx=gcx,
|
||
global_cy=gcy,
|
||
)
|
||
|
||
x0, y0 = pos[nid]
|
||
global0 = count_edge_crossings(pos, links)
|
||
local0 = crossings_involving_node(nid, pos, links, adj)
|
||
stretch0 = _incident_stretch(nid, pos, adj, target_nn)
|
||
|
||
samples = _inward_samples(x0, y0, ax, ay, max_pull=pull)
|
||
# Also try unit centroid if neighbor anchor was used.
|
||
if anchor_kind == "neighbors":
|
||
for gk in node_groups.get(nid, ()):
|
||
if gk in centroids:
|
||
cx, cy = centroids[gk]
|
||
samples.extend(_inward_samples(x0, y0, cx, cy, max_pull=pull))
|
||
|
||
seen: set[tuple[int, int]] = set()
|
||
uniq: list[tuple[float, float, float, float]] = []
|
||
for sx, sy, r, ang in samples:
|
||
key = (int(round(sx)), int(round(sy)))
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
uniq.append((sx, sy, r, ang))
|
||
if len(uniq) >= cand_cap:
|
||
break
|
||
|
||
scored: list[dict[str, Any]] = []
|
||
for sx, sy, r, ang in uniq:
|
||
c = _eval_candidate(
|
||
nid,
|
||
(sx, sy),
|
||
r,
|
||
ang,
|
||
pos=pos,
|
||
names=names,
|
||
links=links,
|
||
adj=adj,
|
||
global0=global0,
|
||
local0=local0,
|
||
target_nn=target_nn,
|
||
nn_floor=nn_floor,
|
||
nbs_cap=nbs_cap,
|
||
)
|
||
if c is None:
|
||
continue
|
||
# Densify: reject moves that raise global crossings.
|
||
if int(c["delta"]["global"]) > 0:
|
||
continue
|
||
scored.append(c)
|
||
|
||
scored.sort(key=_densify_score_key)
|
||
# Prefer stretch drop among non-rising-x candidates.
|
||
improving = [
|
||
c
|
||
for c in scored
|
||
if c["delta"]["global"] <= 0 and float(c.get("stretch") or 99) < stretch0 - 1e-4
|
||
]
|
||
pool = improving if improving else scored
|
||
top = _diversify_top(pool, k=max(1, int(top_k)), min_angle_sep=min_angle_sep)
|
||
|
||
return {
|
||
"ok": True,
|
||
"node_id": nid,
|
||
"name": names.get(nid, nid),
|
||
"x0": round(x0, 1),
|
||
"y0": round(y0, 1),
|
||
"degree": len(adj.get(nid, ())),
|
||
"anchor": {"x": round(ax, 1), "y": round(ay, 1), "kind": anchor_kind},
|
||
"stretch_before": round(stretch0, 3),
|
||
"crossings_before": {"global": int(global0), "incident": int(local0)},
|
||
"candidates": top,
|
||
"sampled": len(seen),
|
||
"improving_n": len(improving),
|
||
"max_pull": pull,
|
||
"nn_floor": nn_floor,
|
||
"hint": (
|
||
"prefer rank1 (inward, x not up, stretch down); pick 2/3 if label/util. "
|
||
"apply with params.pick=1|2|3."
|
||
),
|
||
}
|
||
|
||
|
||
def apply_densify_pick(
|
||
state: LayoutState,
|
||
sweep: dict[str, Any],
|
||
*,
|
||
pick: int = 1,
|
||
) -> OpResult:
|
||
st = state.copy()
|
||
nid = str(sweep.get("node_id") or "")
|
||
cands = list(sweep.get("candidates") or [])
|
||
if not nid or nid not in st.positions or not cands:
|
||
return OpResult(
|
||
state=st,
|
||
moved=set(),
|
||
op="densify_sweep",
|
||
note="densify_sweep:noop",
|
||
params={"error": "no_candidates"},
|
||
)
|
||
idx = max(1, min(int(pick), len(cands))) - 1
|
||
chosen = cands[idx]
|
||
st.positions[nid] = (float(chosen["x"]), float(chosen["y"]))
|
||
st.last_moved = {nid}
|
||
st.meta["densify_sweep"] = {
|
||
"node_id": nid,
|
||
"pick": idx + 1,
|
||
"candidate": chosen,
|
||
"crossings_before": sweep.get("crossings_before"),
|
||
}
|
||
return OpResult(
|
||
state=st,
|
||
moved={nid},
|
||
op="densify_sweep",
|
||
params={
|
||
"node_id": nid,
|
||
"pick": idx + 1,
|
||
"candidate": chosen,
|
||
"crossings_before": sweep.get("crossings_before"),
|
||
},
|
||
note=(
|
||
f"densify_sweep pick={idx + 1} "
|
||
f"g{sweep.get('crossings_before', {}).get('global')}->"
|
||
f"{chosen['crossings']['global']}"
|
||
),
|
||
)
|
||
|
||
|
||
def densify_sweep_round(
|
||
state: LayoutState,
|
||
*,
|
||
params: LayoutParams | None = None,
|
||
top_n: int = 16,
|
||
max_degree: int = 8,
|
||
max_pull: float | None = None,
|
||
nn_floor: float = 60.0,
|
||
min_angle_sep: float = 35.0,
|
||
protect_rigid: bool | str = "off",
|
||
frozen_ids: set[str] | None = None,
|
||
focus_ids: list[str] | None = None,
|
||
groups: list[dict[str, Any]] | None = None,
|
||
) -> OpResult:
|
||
"""Scan sparse/high-stretch nodes; auto-apply #1 if x not up and stretch drops.
|
||
|
||
Default ``protect_rigid=off`` (may move portals). Opt in with portals/all.
|
||
"""
|
||
params = params or LayoutParams()
|
||
st = state.copy()
|
||
pos = dict(st.positions)
|
||
names = dict(st.names)
|
||
links = list(st.links)
|
||
adj = {n: set(st.adj.get(n, ())) for n in pos}
|
||
target_nn = float(getattr(params, "target_nn", 155.0) or 155.0)
|
||
|
||
frozen = _resolve_frozen(st, protect_rigid, frozen_ids)
|
||
|
||
groups = groups if groups is not None else _groups_from_state(st)
|
||
valid = set(pos)
|
||
node_groups, shared, _ = _membership_maps(groups, valid)
|
||
centroids = _exclusive_centroids(groups, pos, shared)
|
||
if groups and not _protect_is_off(protect_rigid):
|
||
frozen |= shared
|
||
|
||
gcx = sum(p[0] for p in pos.values()) / max(len(pos), 1)
|
||
gcy = sum(p[1] for p in pos.values()) / max(len(pos), 1)
|
||
global0 = count_edge_crossings(pos, links)
|
||
|
||
focus = {str(x) for x in (focus_ids or []) if str(x)}
|
||
scored_nodes: list[tuple[float, str]] = []
|
||
for nid in pos:
|
||
if nid in frozen:
|
||
continue
|
||
deg = len(adj.get(nid, ()))
|
||
if deg >= max_degree or deg == 0:
|
||
continue
|
||
if groups and nid not in node_groups and not focus:
|
||
continue
|
||
stretch = _incident_stretch(nid, pos, adj, target_nn)
|
||
ax, ay, _ = _anchor_for_node(
|
||
nid,
|
||
pos,
|
||
adj,
|
||
node_groups=node_groups,
|
||
centroids=centroids,
|
||
global_cx=gcx,
|
||
global_cy=gcy,
|
||
)
|
||
dist = math.hypot(pos[nid][0] - ax, pos[nid][1] - ay)
|
||
if stretch < 1.15 and dist < 180 and nid not in focus:
|
||
continue
|
||
# Higher stretch / farther from anchor first; focus boost.
|
||
pri = 0.0 if nid in focus else 1.0
|
||
scored_nodes.append((-stretch * 10 - dist / 500.0 + pri * 100, nid))
|
||
scored_nodes.sort()
|
||
movable = [nid for _, nid in scored_nodes[: max(1, int(top_n))]]
|
||
|
||
moved: set[str] = set()
|
||
trace: list[dict[str, Any]] = []
|
||
for nid in movable:
|
||
st.positions = pos
|
||
sweep = densify_sweep_node(
|
||
st,
|
||
nid,
|
||
params=params,
|
||
max_pull=max_pull,
|
||
nn_floor=nn_floor,
|
||
min_angle_sep=min_angle_sep,
|
||
protect_rigid="off",
|
||
frozen_ids=frozen,
|
||
groups=groups,
|
||
)
|
||
if not sweep.get("ok"):
|
||
trace.append({"node_id": nid, "skipped": sweep.get("error")})
|
||
continue
|
||
cands = list(sweep.get("candidates") or [])
|
||
if not cands:
|
||
trace.append({"node_id": nid, "skipped": "no_candidates"})
|
||
continue
|
||
best = cands[0]
|
||
stretch_b = float(sweep.get("stretch_before") or 0)
|
||
stretch_a = float(best.get("stretch") or 0)
|
||
if int(best["delta"]["global"]) > 0:
|
||
trace.append(
|
||
{
|
||
"node_id": nid,
|
||
"skipped": "crossing_up",
|
||
"best_delta": best["delta"],
|
||
}
|
||
)
|
||
continue
|
||
if stretch_a >= stretch_b - 1e-4:
|
||
# Allow pure global drop even if stretch flat.
|
||
if int(best["delta"]["global"]) >= 0:
|
||
trace.append(
|
||
{
|
||
"node_id": nid,
|
||
"skipped": "no_stretch_gain",
|
||
"stretch_before": stretch_b,
|
||
"stretch_after": stretch_a,
|
||
}
|
||
)
|
||
continue
|
||
pos[nid] = (float(best["x"]), float(best["y"]))
|
||
moved.add(nid)
|
||
trace.append(
|
||
{
|
||
"node_id": nid,
|
||
"name": names.get(nid, nid),
|
||
"applied": True,
|
||
"pick": 1,
|
||
"xy": [best["x"], best["y"]],
|
||
"delta": best["delta"],
|
||
"stretch": {"before": stretch_b, "after": stretch_a},
|
||
}
|
||
)
|
||
|
||
st.positions = pos
|
||
st.last_moved = moved
|
||
end_g = count_edge_crossings(pos, links)
|
||
meta = {
|
||
"start_crossings": global0,
|
||
"end_crossings": end_g,
|
||
"moved_n": len(moved),
|
||
"scanned_n": len(movable),
|
||
"trace": trace,
|
||
"top_n": top_n,
|
||
"max_degree": max_degree,
|
||
"nn_floor": nn_floor,
|
||
}
|
||
st.meta["densify_sweep"] = meta
|
||
return OpResult(
|
||
state=st,
|
||
moved=moved,
|
||
op="densify_sweep_round",
|
||
params=meta,
|
||
note=f"densify_sweep_round {global0}->{end_g} moved={len(moved)}/{len(movable)}",
|
||
)
|
||
|
||
|
||
def densify_corridor_scan(
|
||
state: LayoutState,
|
||
*,
|
||
groups: list[dict[str, Any]] | None = None,
|
||
corridor_caps: list[float] | None = None,
|
||
pulls: list[float] | None = None,
|
||
iters: int = 6,
|
||
x_slack: int | None = None,
|
||
) -> OpResult:
|
||
"""Sweep corridor_cap×pull; keep best util with ov=0 and tight crossing slack."""
|
||
st0 = state.copy()
|
||
groups = groups if groups is not None else _groups_from_state(st0)
|
||
before = score_state(st0)
|
||
before_util = float((before.get("summary") or {}).get("util") or 0.0)
|
||
before_x = int((before.get("summary") or {}).get("crossings") or 0)
|
||
before_ov = int((before.get("summary") or {}).get("overlaps") or 0)
|
||
if before_ov == 0:
|
||
before_ov = len(overlapping_nodes(st0))
|
||
xs = [p[0] for p in st0.positions.values()]
|
||
ys = [p[1] for p in st0.positions.values()]
|
||
area0 = max(max(xs) - min(xs), 1e-6) * max(max(ys) - min(ys), 1e-6)
|
||
|
||
caps = list(corridor_caps) if corridor_caps else list(_DEFAULT_CORRIDOR_CAPS)
|
||
pull_list = list(pulls) if pulls else list(_DEFAULT_PULLS)
|
||
slack = (
|
||
max(5, int(before_x * 0.05))
|
||
if x_slack is None
|
||
else max(0, int(x_slack))
|
||
)
|
||
|
||
trials: list[dict[str, Any]] = []
|
||
best: dict[str, Any] | None = None
|
||
for cap in caps:
|
||
for pull in pull_list:
|
||
op = shrink_long_corridors(
|
||
st0,
|
||
edge_len_cap=float(cap),
|
||
pull=float(pull),
|
||
iters=max(1, int(iters)),
|
||
max_bridges=8 if groups else 12,
|
||
min_island=2 if groups else 6,
|
||
groups=groups or None,
|
||
accept_crossings=True,
|
||
)
|
||
fin = score_state(op.state)
|
||
util = float((fin.get("summary") or {}).get("util") or 0.0)
|
||
x1 = int((fin.get("summary") or {}).get("crossings") or 0)
|
||
ov = len(overlapping_nodes(op.state))
|
||
xs1 = [p[0] for p in op.state.positions.values()]
|
||
ys1 = [p[1] for p in op.state.positions.values()]
|
||
area1 = max(max(xs1) - min(xs1), 1e-6) * max(max(ys1) - min(ys1), 1e-6)
|
||
area_ratio = area0 / area1
|
||
row = {
|
||
"corridor_cap": float(cap),
|
||
"pull": float(pull),
|
||
"util": util,
|
||
"crossings": x1,
|
||
"overlaps": ov,
|
||
"moved_n": len(op.moved),
|
||
"bbox_area_ratio": round(area_ratio, 4),
|
||
"note": op.note,
|
||
}
|
||
trials.append(row)
|
||
if ov > 0:
|
||
continue
|
||
if x1 > before_x + slack:
|
||
continue
|
||
# Accept if util rises at all, or bbox shrinks ≥0.8% (metro
|
||
# corridors often move area×1.01 before util clears 1%).
|
||
util_up = util > before_util + 1e-6
|
||
area_up = area_ratio >= 1.008
|
||
if not util_up and not area_up:
|
||
continue
|
||
rank = (util, area_ratio, -x1)
|
||
if best is None or rank > best["rank"]:
|
||
best = {
|
||
"rank": rank,
|
||
"state": op.state,
|
||
"moved": set(op.moved),
|
||
"row": row,
|
||
}
|
||
|
||
if best is None:
|
||
meta = {
|
||
"start_crossings": before_x,
|
||
"end_crossings": before_x,
|
||
"start_util": before_util,
|
||
"end_util": before_util,
|
||
"reverted": True,
|
||
"reason": "no_util_gain",
|
||
"trials": trials,
|
||
"x_slack": slack,
|
||
}
|
||
st0.meta["densify_corridor"] = meta
|
||
return OpResult(
|
||
state=st0,
|
||
moved=set(),
|
||
op="densify_corridor_scan",
|
||
params=meta,
|
||
note="densify_corridor_scan:reverted no_util_gain",
|
||
)
|
||
|
||
st = best["state"]
|
||
fin = score_state(st)
|
||
end_util = float((fin.get("summary") or {}).get("util") or 0.0)
|
||
end_x = int((fin.get("summary") or {}).get("crossings") or 0)
|
||
meta = {
|
||
"start_crossings": before_x,
|
||
"end_crossings": end_x,
|
||
"start_util": before_util,
|
||
"end_util": end_util,
|
||
"reverted": False,
|
||
"chosen": best["row"],
|
||
"trials": trials,
|
||
"x_slack": slack,
|
||
"moved_n": len(best["moved"]),
|
||
}
|
||
st.meta = dict(st.meta or {})
|
||
st.meta["densify_corridor"] = meta
|
||
st.last_moved = best["moved"]
|
||
return OpResult(
|
||
state=st,
|
||
moved=best["moved"],
|
||
op="densify_corridor_scan",
|
||
params=meta,
|
||
note=(
|
||
f"densify_corridor_scan util {before_util:.4f}->{end_util:.4f} "
|
||
f"x {before_x}->{end_x} "
|
||
f"cap={best['row']['corridor_cap']:.0f} pull={best['row']['pull']}"
|
||
),
|
||
)
|
||
|
||
|
||
def densify_params_from_overrides(overrides: dict[str, Any] | None) -> dict[str, Any]:
|
||
o = overrides or {}
|
||
out: dict[str, Any] = {}
|
||
if o.get("node_id") is not None:
|
||
out["node_id"] = str(o.get("node_id") or "").strip()
|
||
elif o.get("fabric_node_id") is not None:
|
||
out["node_id"] = str(o.get("fabric_node_id") or "").strip()
|
||
if o.get("pick") is not None:
|
||
try:
|
||
out["pick"] = max(1, min(3, int(o["pick"])))
|
||
except (TypeError, ValueError):
|
||
out["pick"] = 1
|
||
for flag in ("round", "corridor"):
|
||
if o.get(flag) is not None:
|
||
out[flag] = str(o.get(flag)).lower() not in {
|
||
"0",
|
||
"false",
|
||
"no",
|
||
"off",
|
||
"",
|
||
} or o.get(flag) is True
|
||
phase = str(o.get("phase") or "").strip().lower()
|
||
if phase in {"corridor", "corridors", "shrink"}:
|
||
out["corridor"] = True
|
||
elif phase in {"intra", "node", "round"}:
|
||
out["round"] = out.get("round", True)
|
||
for key, cast, default in (
|
||
("top_n", int, 16),
|
||
("max_degree", int, 8),
|
||
("cand_cap", int, 180),
|
||
("top_k", int, 3),
|
||
("iters", int, 6),
|
||
):
|
||
if key not in o or o[key] is None:
|
||
out[key] = default
|
||
continue
|
||
try:
|
||
out[key] = cast(o[key])
|
||
except (TypeError, ValueError):
|
||
out[key] = default
|
||
if o.get("max_pull") is not None:
|
||
try:
|
||
out["max_pull"] = float(o["max_pull"])
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if o.get("nn_floor") is not None:
|
||
try:
|
||
out["nn_floor"] = float(o["nn_floor"])
|
||
except (TypeError, ValueError):
|
||
out["nn_floor"] = 60.0
|
||
else:
|
||
out["nn_floor"] = 60.0
|
||
if o.get("min_angle_sep") is not None:
|
||
try:
|
||
out["min_angle_sep"] = float(o["min_angle_sep"])
|
||
except (TypeError, ValueError):
|
||
out["min_angle_sep"] = 35.0
|
||
else:
|
||
out["min_angle_sep"] = 35.0
|
||
if "protect_rigid" in o:
|
||
v = o["protect_rigid"]
|
||
if isinstance(v, bool):
|
||
out["protect_rigid"] = "portals" if v else "off"
|
||
else:
|
||
key = str(v).strip().lower()
|
||
if key in {"0", "false", "no", "off", "none"}:
|
||
out["protect_rigid"] = "off"
|
||
elif key in {"1", "true", "yes", "on", "portals", "skeleton"}:
|
||
out["protect_rigid"] = "portals"
|
||
elif key in {"all", "full", "rigid"}:
|
||
out["protect_rigid"] = "all"
|
||
else:
|
||
out["protect_rigid"] = key
|
||
else:
|
||
# densify breaks rigid by default (opt in with protect_rigid=portals).
|
||
out["protect_rigid"] = "off"
|
||
focus = o.get("focus_ids") or o.get("focus_node_ids")
|
||
if isinstance(focus, list):
|
||
out["focus_ids"] = [str(x).strip() for x in focus if str(x).strip()]
|
||
raw_p = o.get("portal_ids")
|
||
if isinstance(raw_p, list):
|
||
out["frozen_ids"] = {str(x) for x in raw_p if str(x)}
|
||
caps = o.get("corridor_caps") or o.get("corridor_cap_list")
|
||
if isinstance(caps, list) and caps:
|
||
try:
|
||
out["corridor_caps"] = [float(x) for x in caps]
|
||
except (TypeError, ValueError):
|
||
pass
|
||
elif o.get("corridor_cap") is not None:
|
||
try:
|
||
out["corridor_caps"] = [float(o["corridor_cap"])]
|
||
except (TypeError, ValueError):
|
||
pass
|
||
pulls = o.get("pulls") or o.get("pull_list")
|
||
if isinstance(pulls, list) and pulls:
|
||
try:
|
||
out["pulls"] = [float(x) for x in pulls]
|
||
except (TypeError, ValueError):
|
||
pass
|
||
elif o.get("pull") is not None:
|
||
try:
|
||
out["pulls"] = [float(o["pull"])]
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if o.get("x_slack") is not None:
|
||
try:
|
||
out["x_slack"] = int(o["x_slack"])
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return out
|