mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Update topology tool skill
This commit is contained in:
parent
37dd43923f
commit
99a262e348
4 changed files with 212 additions and 7 deletions
|
|
@ -27,7 +27,7 @@ analyze(structure) → 认 dual_units / shape
|
|||
→ 下一批 sink…(禁止 until_empty 日常连抽)
|
||||
```
|
||||
|
||||
停手:`overlaps=0` 且 `verdict.total≈70`、cpl 中档内即可交付。
|
||||
停手:`overlaps=0` 且 `verdict.total≈70`、cpl 中档、`edge_clearance.status≠fail`、`edge_axis.status≠fail` 方可交付。**勿只看 crossing**——orbit_sweep 的 `improving_n=0` 只代表交叉维度卡壳,须逐项检查 edge_clearance/edge_axis/rings,任一 fail 即继续处理。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -60,6 +60,41 @@ analyzeTopologyViewLayout({ view_id, detail: "structure" })
|
|||
|
||||
---
|
||||
|
||||
## 分层布局(特殊场景)
|
||||
|
||||
当网络需呈现层级结构(外部→接入→核心→汇聚→客户等)时,优化策略不同于通用 mesh:
|
||||
|
||||
### 分层通用规则
|
||||
1. **顶层:外部/对接网络**
|
||||
2. **次顶层:终端客户接入层**
|
||||
3. **中间层:核心层**
|
||||
4. **核心下层:汇聚层**
|
||||
5. **底层:接入层/孤立层**
|
||||
|
||||
### 优化流程
|
||||
1. **先手动后算法**:用户手动拖拽保证业务结构,再用算法优化
|
||||
2. **分层约束**:用 `updateTopologyViewPositions` 按层级设定 y 坐标(同层 y 落在区间内可错落,勿跨层);`orbit_sweep` 传 `y_min/y_max` 约束搜索在本层区间内
|
||||
3. **分步迭代**:
|
||||
- `polish_crossings` 减少交叉(可能破坏分层)
|
||||
- 若分层被破坏,**保留 x 坐标,恢复 y 坐标**重新分层
|
||||
- `clear_edge_hits` 消除贴边
|
||||
- `fix_overlaps` 修复重叠
|
||||
|
||||
### 冲突处理原则
|
||||
| 冲突场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 交叉 vs 结构 | **结构优先**,接受少量跨层交叉 |
|
||||
| 算法 vs 手动 | **手动优先**,算法辅助 |
|
||||
| polish 破坏分层 | 保留 x 坐标,恢复 y 坐标分层 |
|
||||
|
||||
### 关键经验
|
||||
1. **跨层交叉是结构性的**:跨层连线必然穿越中间层,属正常
|
||||
2. **外层节点必须在外部层**:与 PS/PE 连接的外部网元(RNC、华为设备等)应放在最上方
|
||||
3. **orbit_sweep 卡壳更早**:分层约束下 `improving_n=0` 来得快;改传 `objective=total` 让 orbit_sweep 按 crossing+edge_clearance 综合排序
|
||||
4. **验收以层级清晰为先**:`verdict.total` 次于结构可读性;同层 y 在区间内、层间不串层即可交付
|
||||
|
||||
---
|
||||
|
||||
## 根图 → 子区域排水
|
||||
|
||||
```
|
||||
|
|
@ -96,7 +131,7 @@ sinkTopologyDualUnits({
|
|||
| `layout` | 小图配方:`compact` / `corridor` / `rings` / `unstick` |
|
||||
| `layout_dual_unit` | 双门户眼形;单元内交叉≠0 拒绝 |
|
||||
| `move_nodes` / `sink_nodes` | 指定 ids 双向迁移;`park` 块扫 |
|
||||
| `orbit_sweep` | 压交叉;`round` + 大 `max_jump`(约 1800–2800) |
|
||||
| `orbit_sweep` | 压交叉;`round` + 大 `max_jump`(约 1800–2800);`objective=total` 综合 crossing+edge_clearance;`y_min/y_max` 分层约束 |
|
||||
| `polish_crossings` | 一键:straighten→press→untangle |
|
||||
| `clear_edge_hits` | 网元贴非关联边时正交弹开 |
|
||||
| `fix_overlaps` / `resolve_overlaps` | 只拉开重叠 |
|
||||
|
|
@ -104,19 +139,35 @@ sinkTopologyDualUnits({
|
|||
| `straighten_channels` | 拉直 deg≤2 走廊 |
|
||||
| `job_status` / `job_cancel` | 后台 job |
|
||||
|
||||
阶段2顺序:先 `orbit_sweep` 压交叉 → `polish_crossings` → 看 `edge_clearance` 再 `clear_edge_hits`。
|
||||
阶段2是多目标循环,非单向链:
|
||||
|
||||
1. `orbit_sweep` 压交叉 → `polish_crossings`
|
||||
2. **orbit_sweep `improving_n=0` 时勿停**——立即转看 `edge_clearance`(贴边对视觉可读性影响 ≥ 交叉)→ `clear_edge_hits` 正交弹开
|
||||
3. 再看 `edge_axis`(斜边过多)→ `straighten_channels` / 手拖归轴
|
||||
4. 回头复检 crossing 是否因上步变动出现新机会 → 再 `orbit_sweep`
|
||||
|
||||
`orbit_round` 只在全局交叉严格下降时落笔;卡顿加大 `max_jump` / 单点 preview→pick。
|
||||
|
||||
**关键**:单节点 `updateTopologyViewPositions` 拖动时,某节点移动可能增交叉但解多个贴边——以 `verdict.total` 升降为准,勿只盯 crossing 数。`drag_candidates` 可能为空,此时看 `edge_clearance.top` / `crossing.top_nodes` 自行判断拖谁。
|
||||
|
||||
---
|
||||
|
||||
## 验收
|
||||
|
||||
| 块 | 看什么 |
|
||||
|----|--------|
|
||||
| `overlap` | 硬零 |
|
||||
| `crossing` | crossings/cpl;`top_nodes` / `top_edges` |
|
||||
| `edge_clearance` | 贴边 → clear_edge_hits |
|
||||
| `verdict.total` | ≈70 可交付(ov=0) |
|
||||
| `overlap` | 硬零(权重 0.24,硬门控) |
|
||||
| `crossing` | crossings/cpl;`top_nodes` / `top_edges`(权重 0.18) |
|
||||
| `rings` | 最小环被穿(权重 0.10) |
|
||||
| `edge_clearance` | 贴边 → clear_edge_hits;**权重 0.08 但视觉影响 ≥ crossing,优先处理** |
|
||||
| `edge_axis` | 斜边过多 → straighten_channels / 手拖归轴(权重 0.06) |
|
||||
| `verdict.total` | ≈70 可交付(ov=0);**总分升降为准,勿只盯 crossing** |
|
||||
|
||||
### 评分优化快速方法
|
||||
1. **消除 overlap** → 硬门控项,必须为 0
|
||||
2. **减少 crossings** → polish_crossings 大幅降低
|
||||
3. **处理 edge_clearance** → clear_edge_hits 消除贴边
|
||||
4. **结构检查** → 分层清晰 > 交叉最少
|
||||
|
||||
图标 25px;推荐中心距 Δx≥200、Δy≥170。交叉 = 无向 NE↔NE 真交叉(共端点不算)。
|
||||
|
||||
|
|
@ -139,6 +190,43 @@ sinkTopologyDualUnits({
|
|||
|
||||
---
|
||||
|
||||
## 通用优化策略(实战经验)
|
||||
|
||||
### 算法与手动的最佳组合
|
||||
```
|
||||
Step 1: 用户手动拖拽 → 保证业务结构和分层
|
||||
Step 2: polish_crossings → 大幅减少交叉(接受可能破坏分层)
|
||||
Step 3: 检查分层 → 若被破坏,保留 x 恢复 y 重新分层
|
||||
Step 4: clear_edge_hits → 处理贴边问题
|
||||
Step 5: fix_overlaps → 修复节点重叠
|
||||
Step 6: 最终验收 → 结构清晰 > 交叉最少
|
||||
```
|
||||
|
||||
### 工具使用优先级
|
||||
| 场景 | 首选工具 | 参数建议 |
|
||||
|-----|---------|---------|
|
||||
| 整体减交叉 | `polish_crossings` | `top_n=10, max_moves=50` |
|
||||
| 单点微调 | `orbit_sweep` | `objective=total, y_min/y_max` |
|
||||
| 处理贴边 | `clear_edge_hits` | `top_n=15, max_moves=30` |
|
||||
| 修复重叠 | `fix_overlaps` | 直接调用 |
|
||||
| 批量调坐标 | `updateTopologyViewPositions` | 保留 x,恢复 y |
|
||||
|
||||
### 常见问题解决方案
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| polish 破坏分层 | 算法优先减少交叉 | 保留 x 坐标,恢复 y 分层 |
|
||||
| orbit_sweep 无改善 | 位置已优化 | 接受现状或手动调整 |
|
||||
| 节点重叠 | 坐标调整太近 | fix_overlaps 自动修复 |
|
||||
| 交叉突然增加 | clear_edge_hits 移动节点 | 重新 polish_crossings |
|
||||
|
||||
### 关键原则
|
||||
1. **业务结构优先于算法优化**:网络拓扑的分层结构比最少交叉更重要
|
||||
2. **局部微调优于全局重置**:用 `orbit_sweep`/`updateTopologyViewPositions` 单点调整,而非 `layout` 全局布局
|
||||
3. **预览模式优先于应用模式**:先用 `mode=preview` 查看效果,确认后再 `mode=apply`
|
||||
4. **分步迭代优于一次性操作**:polish → clear_edge_hits → fix_overlaps 分步骤执行
|
||||
|
||||
---
|
||||
|
||||
## 代码热更
|
||||
|
||||
1. 本仓 MCP 用 `PYTHONPATH=…/src`,改源码后不必为加载而 pip install。
|
||||
|
|
|
|||
|
|
@ -184,6 +184,40 @@ def _score_key(c: dict[str, Any]) -> tuple:
|
|||
)
|
||||
|
||||
|
||||
def _rerank_by_total(
|
||||
scored: list[dict[str, Any]],
|
||||
nid: str,
|
||||
pos: dict[str, tuple[float, float]],
|
||||
names: dict[str, str],
|
||||
links: list[tuple[str, str]],
|
||||
adj: dict[str, set[str]],
|
||||
*,
|
||||
re_rank_n: int = 20,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Re-rank top candidates by crossing + edge_clearance (multi-objective).
|
||||
|
||||
A move may increase crossings but resolve several edge-clearance hits;
|
||||
ranking by ``crossings + edge_clearance_hits`` aligns with verdict.total.
|
||||
"""
|
||||
from netx_topology_mcp.layout_metrics import compute_edge_clearance
|
||||
|
||||
n = min(re_rank_n, len(scored))
|
||||
for c in scored[:n]:
|
||||
trial = dict(pos)
|
||||
trial[nid] = (float(c["x"]), float(c["y"]))
|
||||
ec = compute_edge_clearance(trial, links, names=names, top_n=1)
|
||||
c["edge_clearance_hits"] = int(ec.get("edge_clearance_hits") or 0)
|
||||
head = sorted(
|
||||
scored[:n],
|
||||
key=lambda c: (
|
||||
int(c["crossings"]["global"]) + int(c.get("edge_clearance_hits", 0)),
|
||||
int(c["crossings"]["incident"]),
|
||||
float(c.get("stretch") or 1.0),
|
||||
),
|
||||
)
|
||||
return head + scored[n:]
|
||||
|
||||
|
||||
def _diversify_top(
|
||||
ranked: list[dict[str, Any]],
|
||||
*,
|
||||
|
|
@ -286,6 +320,9 @@ def orbit_sweep_node(
|
|||
protect_rigid: bool | str = "off",
|
||||
frozen_ids: set[str] | None = None,
|
||||
top_k: int = 3,
|
||||
y_min: float | None = None,
|
||||
y_max: float | None = None,
|
||||
objective: str = "crossing",
|
||||
) -> dict[str, Any]:
|
||||
"""Sweep polar candidates for one node; return diversified top-k.
|
||||
|
||||
|
|
@ -370,6 +407,15 @@ def orbit_sweep_node(
|
|||
if len(uniq) >= cand_cap:
|
||||
break
|
||||
|
||||
# Layered y constraint: skip candidates outside [y_min, y_max]
|
||||
if y_min is not None or y_max is not None:
|
||||
uniq = [
|
||||
(sx, sy, r, ang)
|
||||
for sx, sy, r, ang in uniq
|
||||
if (y_min is None or sy >= y_min)
|
||||
and (y_max is None or sy <= y_max)
|
||||
]
|
||||
|
||||
scored: list[dict[str, Any]] = []
|
||||
for sx, sy, r, ang in uniq:
|
||||
c = _eval_candidate(
|
||||
|
|
@ -417,6 +463,8 @@ def orbit_sweep_node(
|
|||
break
|
||||
|
||||
for sx, sy, r, ang in fine:
|
||||
if (y_min is not None and sy < y_min) or (y_max is not None and sy > y_max):
|
||||
continue
|
||||
key = (int(round(sx)), int(round(sy)))
|
||||
if key in seen:
|
||||
continue
|
||||
|
|
@ -440,6 +488,9 @@ def orbit_sweep_node(
|
|||
scored.append(c)
|
||||
|
||||
scored.sort(key=_score_key)
|
||||
# Multi-objective re-rank: optimize total score, not just crossings
|
||||
if objective == "total" and len(scored) > 1:
|
||||
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]
|
||||
pool = improving if improving else scored
|
||||
|
|
@ -458,9 +509,16 @@ def orbit_sweep_node(
|
|||
"improving_n": len(improving),
|
||||
"max_jump": jump,
|
||||
"angle_step": angle_step,
|
||||
"objective": objective,
|
||||
"y_band": (
|
||||
None if (y_min is None and y_max is None)
|
||||
else [y_min, y_max]
|
||||
),
|
||||
"hint": (
|
||||
"prefer rank1 unless util/label concern; then pick 2/3. "
|
||||
"apply with params.pick=1|2|3 or updateTopologyViewPositions."
|
||||
+ (" objective=total: rank by crossing+edge_clearance, may trade crossings for clearance."
|
||||
if objective == "total" else "")
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -736,6 +794,16 @@ def orbit_params_from_overrides(overrides: dict[str, Any] | None) -> dict[str, A
|
|||
raw_p = o.get("portal_ids")
|
||||
if isinstance(raw_p, list):
|
||||
out["frozen_ids"] = {str(x) for x in raw_p if str(x)}
|
||||
# Layered y constraint (y_min/y_max): keep node within its layer band
|
||||
for yk in ("y_min", "y_max"):
|
||||
if o.get(yk) is not None:
|
||||
try:
|
||||
out[yk] = float(o[yk])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
# Multi-objective ranking: "crossing" (default) or "total"
|
||||
obj = str(o.get("objective") or "crossing").strip().lower()
|
||||
out["objective"] = "total" if obj in ("total", "score", "multi") else "crossing"
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,52 @@ def list_crossings(
|
|||
top_e = top_crossing_edges(
|
||||
pos_xy, links, names=names, top_n=5, edge_participation=edge_hit
|
||||
)
|
||||
# Fallback: when no drag_candidates (all hot nodes are high-degree hubs),
|
||||
# suggest from edge_clearance.top and crossing top_nodes regardless of degree.
|
||||
if not candidates:
|
||||
from netx_topology_mcp.layout_metrics import compute_edge_clearance
|
||||
|
||||
ec = compute_edge_clearance(pos_xy, links, names=names, top_n=5)
|
||||
for eh in (ec.get("top_edge_hits") or [])[:5]:
|
||||
nid = str(eh.get("fabric_node_id") or "")
|
||||
if not nid or nid not in pos:
|
||||
continue
|
||||
x, y, name = pos[nid]
|
||||
deg = len(adj.get(nid, ()))
|
||||
candidates.append({
|
||||
"fabric_node_id": nid,
|
||||
"name": name,
|
||||
"short": _short_name(name),
|
||||
"x": round(x, 1),
|
||||
"y": round(y, 1),
|
||||
"degree": deg,
|
||||
"in_crossings": hit.get(nid, 0),
|
||||
"score": 0.0,
|
||||
"suggest_xy": [],
|
||||
"delta_crossings_est": 0,
|
||||
"reason": "edge_clearance_hit",
|
||||
})
|
||||
for tn in top5:
|
||||
nid = str(tn.get("fabric_node_id") or "")
|
||||
if not nid or nid not in pos:
|
||||
continue
|
||||
if any(c["fabric_node_id"] == nid for c in candidates):
|
||||
continue
|
||||
x, y, name = pos[nid]
|
||||
deg = len(adj.get(nid, ()))
|
||||
candidates.append({
|
||||
"fabric_node_id": nid,
|
||||
"name": name,
|
||||
"short": _short_name(name),
|
||||
"x": round(x, 1),
|
||||
"y": round(y, 1),
|
||||
"degree": deg,
|
||||
"in_crossings": tn.get("crossing_hits", 0),
|
||||
"score": round(tn.get("crossing_hits", 0) / max(deg, 1), 3),
|
||||
"suggest_xy": [],
|
||||
"delta_crossings_est": 0,
|
||||
"reason": "top_crossing_high_degree",
|
||||
})
|
||||
return {
|
||||
"edge_crossings": total_cross,
|
||||
"crossings_listed": len(crosses),
|
||||
|
|
|
|||
|
|
@ -512,6 +512,9 @@ def run_layout_on_graph(
|
|||
protect_rigid=protect,
|
||||
frozen_ids=frozen,
|
||||
top_k=int(knobs.get("top_k") or 3),
|
||||
y_min=knobs.get("y_min"),
|
||||
y_max=knobs.get("y_max"),
|
||||
objective=knobs.get("objective", "crossing"),
|
||||
)
|
||||
if not sweep.get("ok"):
|
||||
fin = score_state(st0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue