mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Fix fabric path search, read scope, orbit total ranking, and edge menu clamp.
Prefer BFS shortest paths, allow ne:read on path find, honor objective=total clearance trades, and keep context menus on-screen without a11y clock spam. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
298859e608
commit
e4a135ec16
13 changed files with 163 additions and 30 deletions
|
|
@ -498,21 +498,22 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"name": "findTopologyPaths",
|
||||
"description": (
|
||||
"Find up to max_paths simple paths between two fabric nodes for troubleshooting. "
|
||||
"Accepts ume_ne_id (from UME alarms) or managed_ne_id — resolved to fabric node "
|
||||
"internally. Returns paths with node sequence + edge status (up/down)."
|
||||
"For each endpoint provide exactly one of ume_ne_id (from UME alarms) or "
|
||||
"managed_ne_id — resolved to fabric node internally. Returns shortest paths "
|
||||
"first with node sequence + edge status (up/down)."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from_ume_ne_id": {"type": "string", "description": "Source UME ne_id (from alarm ne_id)"},
|
||||
"from_managed_ne_id": {"type": "string", "description": "Source managed NE id"},
|
||||
"to_ume_ne_id": {"type": "string", "description": "Target UME ne_id"},
|
||||
"to_managed_ne_id": {"type": "string", "description": "Target managed NE id"},
|
||||
"from_ume_ne_id": {"type": "string", "description": "Source UME ne_id (from alarm ne_id); mutually exclusive with from_managed_ne_id"},
|
||||
"from_managed_ne_id": {"type": "string", "description": "Source managed NE id; mutually exclusive with from_ume_ne_id"},
|
||||
"to_ume_ne_id": {"type": "string", "description": "Target UME ne_id; mutually exclusive with to_managed_ne_id"},
|
||||
"to_managed_ne_id": {"type": "string", "description": "Target managed NE id; mutually exclusive with to_ume_ne_id"},
|
||||
"max_paths": {"type": "integer", "minimum": 1, "maximum": 10, "default": 3},
|
||||
"max_hops": {"type": "integer", "minimum": 1, "maximum": 12, "default": 6},
|
||||
"layer": {"type": "string", "default": "physical"},
|
||||
},
|
||||
"required": ["from_ume_ne_id", "to_ume_ne_id"],
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -489,10 +489,24 @@ def orbit_sweep_node(
|
|||
|
||||
scored.sort(key=_score_key)
|
||||
# Multi-objective re-rank: optimize total score, not just crossings
|
||||
if objective == "total" and len(scored) > 1:
|
||||
use_total = objective == "total" and len(scored) > 1
|
||||
base_clearance_hits = 0
|
||||
if use_total:
|
||||
from netx_topology_mcp.layout_metrics import compute_edge_clearance
|
||||
|
||||
ec0 = compute_edge_clearance(pos, links, names=names, top_n=1)
|
||||
base_clearance_hits = int(ec0.get("edge_clearance_hits") or 0)
|
||||
scored = _rerank_by_total(scored, nid, pos, names, links, adj)
|
||||
# Prefer improving moves; still return best even if none improve.
|
||||
improving = [c for c in scored if c["delta"]["global"] < 0]
|
||||
if use_total:
|
||||
base_total = int(global0) + base_clearance_hits
|
||||
improving = [
|
||||
c
|
||||
for c in scored
|
||||
if int(c["crossings"]["global"]) + int(c.get("edge_clearance_hits", 0)) < base_total
|
||||
]
|
||||
else:
|
||||
improving = [c for c in scored if c["delta"]["global"] < 0]
|
||||
pool = improving if improving else scored
|
||||
top = _diversify_top(pool, k=max(1, int(top_k)), min_angle_sep=min_angle_sep)
|
||||
|
||||
|
|
@ -582,8 +596,11 @@ def orbit_sweep_round(
|
|||
protect_rigid: bool | str = "off",
|
||||
frozen_ids: set[str] | None = None,
|
||||
focus_ids: list[str] | None = None,
|
||||
y_min: float | None = None,
|
||||
y_max: float | None = None,
|
||||
objective: str = "crossing",
|
||||
) -> OpResult:
|
||||
"""Scan hot nodes; auto-apply each node's rank-1 if global crossings drop.
|
||||
"""Scan hot nodes; auto-apply each node's rank-1 if the active objective improves.
|
||||
|
||||
Default ``protect_rigid=off`` (may move portals). Opt in with portals/all.
|
||||
"""
|
||||
|
|
@ -653,6 +670,9 @@ def orbit_sweep_round(
|
|||
min_angle_sep=min_angle_sep,
|
||||
protect_rigid="off", # already applied frozen set
|
||||
frozen_ids=frozen,
|
||||
y_min=y_min,
|
||||
y_max=y_max,
|
||||
objective=objective,
|
||||
)
|
||||
if not sweep.get("ok"):
|
||||
trace.append({"node_id": nid, "skipped": sweep.get("error")})
|
||||
|
|
@ -662,12 +682,14 @@ def orbit_sweep_round(
|
|||
trace.append({"node_id": nid, "skipped": "no_candidates"})
|
||||
continue
|
||||
best = cands[0]
|
||||
if int(best["delta"]["global"]) >= 0:
|
||||
# Skip when the active objective has no improving candidates.
|
||||
if int(sweep.get("improving_n") or 0) <= 0:
|
||||
trace.append(
|
||||
{
|
||||
"node_id": nid,
|
||||
"skipped": "no_global_gain",
|
||||
"best_delta": best["delta"],
|
||||
"objective": objective,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -475,6 +475,9 @@ def run_layout_on_graph(
|
|||
protect_rigid=protect,
|
||||
frozen_ids=frozen,
|
||||
focus_ids=knobs.get("focus_ids"),
|
||||
y_min=knobs.get("y_min"),
|
||||
y_max=knobs.get("y_max"),
|
||||
objective=str(knobs.get("objective") or "crossing"),
|
||||
)
|
||||
st = normalize_origin(op.state, base_params).state
|
||||
fin = score_state(st)
|
||||
|
|
|
|||
|
|
@ -174,3 +174,37 @@ def test_orbit_default_protect_off_ignores_portal_freeze() -> None:
|
|||
)
|
||||
assert frozen["ok"] is False
|
||||
assert frozen.get("error") == "frozen"
|
||||
|
||||
|
||||
def test_orbit_objective_total_ranks_clearance_trade() -> None:
|
||||
"""objective=total may keep a crossing-up move if clearance improves enough."""
|
||||
# Hub h near non-incident segment a—b; moving h right cuts clearance hits
|
||||
# but can add a mild cross with c—d. Crossing-only ranking would reject it.
|
||||
nodes = [
|
||||
{"fabric_node_id": "a", "name": "AAAAAA-EN-1", "x": 0.0, "y": 0.0},
|
||||
{"fabric_node_id": "b", "name": "BBBBBB-EN-2", "x": 400.0, "y": 0.0},
|
||||
{"fabric_node_id": "c", "name": "CCCCCC-EN-3", "x": 200.0, "y": -200.0},
|
||||
{"fabric_node_id": "d", "name": "DDDDDD-EN-4", "x": 200.0, "y": 200.0},
|
||||
{"fabric_node_id": "h", "name": "HHHHHH-EN-5", "x": 200.0, "y": 20.0},
|
||||
{"fabric_node_id": "t", "name": "TTTTTT-EN-6", "x": 200.0, "y": 300.0},
|
||||
]
|
||||
edges = [
|
||||
{"a_node_id": "a", "b_node_id": "b"},
|
||||
{"a_node_id": "c", "b_node_id": "d"},
|
||||
{"a_node_id": "h", "b_node_id": "t"},
|
||||
]
|
||||
st = build_state_from_nodes_edges(nodes, edges)
|
||||
st.positions = {n["fabric_node_id"]: (float(n["x"]), float(n["y"])) for n in nodes}
|
||||
|
||||
by_cross = orbit_sweep_node(st, "h", max_jump=400, nn_floor=20.0, objective="crossing")
|
||||
by_total = orbit_sweep_node(st, "h", max_jump=400, nn_floor=20.0, objective="total")
|
||||
assert by_cross["ok"] is True and by_total["ok"] is True
|
||||
assert by_total.get("objective") == "total"
|
||||
# y_band plumbing
|
||||
banded = orbit_sweep_node(
|
||||
st, "h", max_jump=400, nn_floor=20.0, objective="total", y_min=0.0, y_max=80.0
|
||||
)
|
||||
assert banded["ok"] is True
|
||||
assert banded.get("y_band") == [0.0, 80.0]
|
||||
for c in banded.get("candidates") or []:
|
||||
assert 0.0 <= float(c["y"]) <= 80.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue