mirror of
https://github.com/hansjone/netx.git
synced 2026-10-17 01:03:56 +08:00
Converge netx-topology MCP to dual-unit drain + orbit/polish mainline.
Trim public tools to 14, expose only kept layout actions/recipes, merge Fabric queries, drop createTopologyView/listTopologyViews, and align skill/docs with the live catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9bcb8ee0e3
commit
79f929de0c
86 changed files with 31578 additions and 240 deletions
141
packages/netx-topology-mcp/tests/test_force_densify.py
Normal file
141
packages/netx-topology-mcp/tests/test_force_densify.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""Force densify: edge attract + local repulse + semi-rigid bodies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from netx_topology_mcp.layout_ops.force_densify import force_densify_round
|
||||
from netx_topology_mcp.layout_ops.graph_util import build_state_from_nodes_edges
|
||||
from netx_topology_mcp.layout_ops.rigid_units import groups_from_membership
|
||||
from netx_topology_mcp.layout_ops.score import score_state
|
||||
from netx_topology_mcp.layout_tool import ACTIONS
|
||||
|
||||
|
||||
def test_actions_include_force_densify() -> None:
|
||||
assert "force_densify" not in ACTIONS
|
||||
|
||||
|
||||
def _sparse_two_units():
|
||||
nodes = [
|
||||
{"fabric_node_id": "p", "name": "P", "x": 0, "y": 0},
|
||||
{"fabric_node_id": "a1", "name": "A1", "x": 120, "y": 40},
|
||||
{"fabric_node_id": "a2", "name": "A2", "x": 240, "y": 0},
|
||||
{"fabric_node_id": "b1", "name": "B1", "x": 4200, "y": 0},
|
||||
{"fabric_node_id": "b2", "name": "B2", "x": 4400, "y": 80},
|
||||
{"fabric_node_id": "b3", "name": "B3", "x": 4600, "y": -60},
|
||||
]
|
||||
edges = [
|
||||
{"a_node_id": "p", "b_node_id": "a1"},
|
||||
{"a_node_id": "a1", "b_node_id": "a2"},
|
||||
{"a_node_id": "a2", "b_node_id": "b1"},
|
||||
{"a_node_id": "b1", "b_node_id": "b2"},
|
||||
{"a_node_id": "b1", "b_node_id": "b3"},
|
||||
]
|
||||
st = build_state_from_nodes_edges(nodes, edges)
|
||||
groups = groups_from_membership(
|
||||
[
|
||||
("va", ["p", "a1", "a2"]),
|
||||
("vb", ["b1", "b2", "b3"]),
|
||||
]
|
||||
)
|
||||
st.meta = {"compose_views": {"rigid_groups": groups}}
|
||||
return st, groups
|
||||
|
||||
|
||||
def test_force_densify_shrinks_bridge() -> None:
|
||||
st, groups = _sparse_two_units()
|
||||
L0 = math.hypot(
|
||||
st.positions["b1"][0] - st.positions["a2"][0],
|
||||
st.positions["b1"][1] - st.positions["a2"][1],
|
||||
)
|
||||
before = score_state(st, fast=True)
|
||||
util0 = float((before.get("summary") or {}).get("util") or 0.0)
|
||||
|
||||
op = force_densify_round(
|
||||
st,
|
||||
groups=groups,
|
||||
iters=16,
|
||||
step=0.5,
|
||||
max_step=280.0,
|
||||
ideal_len=280.0,
|
||||
nn_floor=90.0,
|
||||
attract_k=1.4,
|
||||
repulse_k=0.6,
|
||||
gravity_k=0.9,
|
||||
rigid_strength=0.95,
|
||||
deform=0.05,
|
||||
protect_rigid="off",
|
||||
x_slack=500,
|
||||
)
|
||||
assert op.op == "force_densify"
|
||||
assert not (op.params or {}).get("reverted"), op.note
|
||||
L1 = math.hypot(
|
||||
op.state.positions["b1"][0] - op.state.positions["a2"][0],
|
||||
op.state.positions["b1"][1] - op.state.positions["a2"][1],
|
||||
)
|
||||
assert L1 < L0 * 0.92, f"bridge not shortened: {L0:.0f}->{L1:.0f}"
|
||||
after = score_state(op.state, fast=True)
|
||||
util1 = float((after.get("summary") or {}).get("util") or 0.0)
|
||||
assert util1 >= util0 - 1e-6
|
||||
|
||||
|
||||
def test_edge_spring_pushes_crushed_edge() -> None:
|
||||
"""Too-short edge should lengthen (push), not only pull long ones."""
|
||||
nodes = [
|
||||
{"fabric_node_id": "a", "name": "A", "x": 0, "y": 0},
|
||||
{"fabric_node_id": "b", "name": "B", "x": 40, "y": 0},
|
||||
{"fabric_node_id": "c", "name": "C", "x": 2000, "y": 0},
|
||||
]
|
||||
edges = [
|
||||
{"a_node_id": "a", "b_node_id": "b"},
|
||||
{"a_node_id": "b", "b_node_id": "c"},
|
||||
]
|
||||
st = build_state_from_nodes_edges(nodes, edges)
|
||||
L0 = math.hypot(
|
||||
st.positions["b"][0] - st.positions["a"][0],
|
||||
st.positions["b"][1] - st.positions["a"][1],
|
||||
)
|
||||
op = force_densify_round(
|
||||
st,
|
||||
groups=[],
|
||||
iters=12,
|
||||
step=0.45,
|
||||
max_step=80.0,
|
||||
ideal_len=160.0,
|
||||
nn_floor=80.0,
|
||||
attract_k=1.5,
|
||||
repulse_k=0.4,
|
||||
gravity_k=0.0,
|
||||
rigid_strength=0.0,
|
||||
deform=0.0,
|
||||
protect_rigid="off",
|
||||
x_slack=200,
|
||||
)
|
||||
L1 = math.hypot(
|
||||
op.state.positions["b"][0] - op.state.positions["a"][0],
|
||||
op.state.positions["b"][1] - op.state.positions["a"][1],
|
||||
)
|
||||
assert L1 > L0 + 15.0, f"crushed edge not pushed: {L0:.1f}->{L1:.1f}"
|
||||
|
||||
|
||||
def test_semi_rigid_preserves_internal_ratios() -> None:
|
||||
st, groups = _sparse_two_units()
|
||||
|
||||
def _pair(pos):
|
||||
d12 = math.hypot(pos["b2"][0] - pos["b1"][0], pos["b2"][1] - pos["b1"][1])
|
||||
d13 = math.hypot(pos["b3"][0] - pos["b1"][0], pos["b3"][1] - pos["b1"][1])
|
||||
return d12 / max(d13, 1e-6)
|
||||
|
||||
r0 = _pair(st.positions)
|
||||
op = force_densify_round(
|
||||
st,
|
||||
groups=groups,
|
||||
iters=10,
|
||||
rigid_strength=1.0,
|
||||
deform=0.0,
|
||||
ideal_len=180.0,
|
||||
attract_k=1.2,
|
||||
x_slack=500,
|
||||
)
|
||||
r1 = _pair(op.state.positions)
|
||||
assert abs(r1 - r0) < 0.05, f"rigid shape drifted: {r0:.3f}->{r1:.3f}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue