mirror of
https://github.com/hansjone/netx.git
synced 2026-10-17 19:23:22 +08:00
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:
parent
918ece86c3
commit
52679a7cfc
150 changed files with 2367 additions and 2812 deletions
591
netx_api/mcp_remote/topology/layout_ops/hierarchy.py
Normal file
591
netx_api/mcp_remote/topology/layout_ops/hierarchy.py
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
"""Top-down hierarchy layout: contract → order sectors → expand.
|
||||
|
||||
At each hub level we only score crossings on the *contracted* graph
|
||||
(parent + one representative per child territory). Internal edges of
|
||||
children are ignored until that child is expanded. Accept/reject is
|
||||
driven by contracted crossings only — global leaf crossings may rise
|
||||
temporarily; that is intentional (fix lower levels later).
|
||||
|
||||
Expand is a *rigid* polar remapping of each stub territory onto the new
|
||||
sector angle so local corridor geometry survives the reorder.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
from netx_api.mcp_remote.topology.layout_metrics import count_edge_crossings, segments_properly_intersect
|
||||
from netx_api.mcp_remote.topology.layout_ops.hotspots import fix_overlaps_local
|
||||
from netx_api.mcp_remote.topology.layout_ops.partition import pick_hub_seeds
|
||||
from netx_api.mcp_remote.topology.layout_ops.state import LayoutParams, LayoutState, OpResult
|
||||
|
||||
|
||||
def _ang(pos: dict[str, tuple[float, float]], hub: str, nid: str) -> float:
|
||||
hx, hy = pos[hub]
|
||||
x, y = pos[nid]
|
||||
return math.atan2(y - hy, x - hx)
|
||||
|
||||
|
||||
def _stub_territories(
|
||||
hub: str,
|
||||
members: set[str],
|
||||
adj: dict[str, set[str]],
|
||||
pinned: set[str],
|
||||
) -> tuple[list[str], dict[str, str]]:
|
||||
stubs = [
|
||||
n
|
||||
for n in adj.get(hub, ())
|
||||
if n in members and n not in pinned
|
||||
]
|
||||
owner: dict[str, str] = {}
|
||||
q: deque[str] = deque()
|
||||
for stub in stubs:
|
||||
owner[stub] = stub
|
||||
q.append(stub)
|
||||
while q:
|
||||
u = q.popleft()
|
||||
for v in adj.get(u, ()):
|
||||
if v not in members or v == hub or v in owner or v in pinned:
|
||||
continue
|
||||
owner[v] = owner[u]
|
||||
q.append(v)
|
||||
return stubs, owner
|
||||
|
||||
|
||||
def _owner_hub_map(
|
||||
adj: dict[str, set[str]],
|
||||
hubs: set[str],
|
||||
) -> dict[str, str]:
|
||||
"""BFS Voronoi: every node maps to nearest hub (ties: first reached)."""
|
||||
owner: dict[str, str] = {h: h for h in hubs}
|
||||
q: deque[str] = deque(hubs)
|
||||
while q:
|
||||
u = q.popleft()
|
||||
ou = owner[u]
|
||||
for v in adj.get(u, ()):
|
||||
if v in owner:
|
||||
continue
|
||||
owner[v] = ou
|
||||
q.append(v)
|
||||
return owner
|
||||
|
||||
|
||||
def _contracted_links(
|
||||
hub: str,
|
||||
stubs: list[str],
|
||||
owner: dict[str, str],
|
||||
adj: dict[str, set[str]],
|
||||
pinned: set[str],
|
||||
*,
|
||||
hub_of: dict[str, str] | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Edges among {hub} ∪ stubs after contracting each stub territory.
|
||||
|
||||
Foreign endpoints outside this hub's territories collapse to their
|
||||
owning hub (Voronoi), so cross-block edges are not dropped from the
|
||||
contracted objective.
|
||||
"""
|
||||
terr: dict[str, set[str]] = {s: {s} for s in stubs}
|
||||
for n, o in owner.items():
|
||||
terr.setdefault(o, set()).add(n)
|
||||
node_to_rep: dict[str, str] = {hub: hub}
|
||||
for s, nodes in terr.items():
|
||||
for n in nodes:
|
||||
node_to_rep[n] = s
|
||||
|
||||
links: set[tuple[str, str]] = set()
|
||||
for s in stubs:
|
||||
links.add(tuple(sorted((hub, s))))
|
||||
for s, nodes in terr.items():
|
||||
for n in nodes:
|
||||
for v in adj.get(n, ()):
|
||||
if v == hub:
|
||||
continue
|
||||
if v in node_to_rep:
|
||||
r = node_to_rep[v]
|
||||
if r != s:
|
||||
links.add(tuple(sorted((s, r))))
|
||||
elif v in pinned:
|
||||
links.add(tuple(sorted((s, v))))
|
||||
elif hub_of is not None and v in hub_of:
|
||||
fh = hub_of[v]
|
||||
if fh != hub:
|
||||
links.add(tuple(sorted((s, fh))))
|
||||
return sorted(links)
|
||||
|
||||
|
||||
def _contracted_crossings(
|
||||
hub: str,
|
||||
stubs_ord: list[str],
|
||||
links: list[tuple[str, str]],
|
||||
*,
|
||||
radius: float,
|
||||
hub_xy: tuple[float, float],
|
||||
foreign_pos: dict[str, tuple[float, float]] | None = None,
|
||||
a0: float | None = None,
|
||||
a1: float | None = None,
|
||||
) -> int:
|
||||
"""Place stub reps on an arc; count crossings of contracted links only."""
|
||||
hx, hy = hub_xy
|
||||
n = len(stubs_ord)
|
||||
if n == 0:
|
||||
return 0
|
||||
if a0 is None or a1 is None:
|
||||
a0, a1 = -math.pi * 0.85, math.pi * 0.85
|
||||
pos: dict[str, tuple[float, float]] = {hub: (hx, hy)}
|
||||
if foreign_pos:
|
||||
pos.update(foreign_pos)
|
||||
for i, s in enumerate(stubs_ord):
|
||||
mid = 0.5 * (a0 + a1) if n == 1 else a0 + (a1 - a0) * i / (n - 1)
|
||||
pos[s] = (hx + math.cos(mid) * radius, hy + math.sin(mid) * radius)
|
||||
segs = [(a, b) for a, b in links if a in pos and b in pos]
|
||||
c = 0
|
||||
for i, (a, b) in enumerate(segs):
|
||||
p1, p2 = pos[a], pos[b]
|
||||
for u, v in segs[i + 1 :]:
|
||||
if len({a, b, u, v}) < 4:
|
||||
continue
|
||||
if segments_properly_intersect(p1, p2, pos[u], pos[v]):
|
||||
c += 1
|
||||
return c
|
||||
|
||||
|
||||
def _score_order(
|
||||
hub: str,
|
||||
stubs_ord: list[str],
|
||||
links: list[tuple[str, str]],
|
||||
*,
|
||||
radius: float,
|
||||
hub_xy: tuple[float, float],
|
||||
foreign_pos: dict[str, tuple[float, float]],
|
||||
a0: float,
|
||||
a1: float,
|
||||
) -> int:
|
||||
return _contracted_crossings(
|
||||
hub,
|
||||
stubs_ord,
|
||||
links,
|
||||
radius=radius,
|
||||
hub_xy=hub_xy,
|
||||
foreign_pos=foreign_pos,
|
||||
a0=a0,
|
||||
a1=a1,
|
||||
)
|
||||
|
||||
|
||||
def _candidate_orders(stubs: list[str], pos: dict[str, tuple[float, float]], hub: str) -> list[list[str]]:
|
||||
if not stubs:
|
||||
return []
|
||||
geo = sorted(stubs, key=lambda s: _ang(pos, hub, s))
|
||||
orders: list[list[str]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
|
||||
def _add(ord_: list[str]) -> None:
|
||||
key = tuple(ord_)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
orders.append(ord_)
|
||||
|
||||
for base in (geo, list(reversed(geo))):
|
||||
for k in range(len(base)):
|
||||
_add(base[k:] + base[:k])
|
||||
# Adjacent swaps from geo.
|
||||
for i in range(len(geo) - 1):
|
||||
trial = list(geo)
|
||||
trial[i], trial[i + 1] = trial[i + 1], trial[i]
|
||||
_add(trial)
|
||||
return orders
|
||||
|
||||
|
||||
def _best_order_2opt(
|
||||
hub: str,
|
||||
stubs: list[str],
|
||||
pos: dict[str, tuple[float, float]],
|
||||
links: list[tuple[str, str]],
|
||||
*,
|
||||
radius: float,
|
||||
hub_xy: tuple[float, float],
|
||||
foreign_pos: dict[str, tuple[float, float]],
|
||||
a0: float,
|
||||
a1: float,
|
||||
) -> tuple[list[str], int]:
|
||||
"""Greedy 2-opt / insert bakeoff seeded from candidate rotations."""
|
||||
seeds = _candidate_orders(stubs, pos, hub)
|
||||
best_ord = sorted(stubs, key=lambda s: _ang(pos, hub, s))
|
||||
best_cc = _score_order(
|
||||
hub, best_ord, links, radius=radius, hub_xy=hub_xy, foreign_pos=foreign_pos, a0=a0, a1=a1
|
||||
)
|
||||
for seed in seeds:
|
||||
cur = list(seed)
|
||||
cur_cc = _score_order(
|
||||
hub, cur, links, radius=radius, hub_xy=hub_xy, foreign_pos=foreign_pos, a0=a0, a1=a1
|
||||
)
|
||||
improved = True
|
||||
rounds = 0
|
||||
while improved and rounds < 12:
|
||||
improved = False
|
||||
rounds += 1
|
||||
n = len(cur)
|
||||
for i in range(n):
|
||||
for j in range(i + 2, n + (0 if i == 0 else 1)):
|
||||
# reverse segment [i:j]
|
||||
jj = j if j <= n else n
|
||||
if jj - i < 2:
|
||||
continue
|
||||
trial = cur[:i] + list(reversed(cur[i:jj])) + cur[jj:]
|
||||
if len(trial) != n:
|
||||
continue
|
||||
cc = _score_order(
|
||||
hub,
|
||||
trial,
|
||||
links,
|
||||
radius=radius,
|
||||
hub_xy=hub_xy,
|
||||
foreign_pos=foreign_pos,
|
||||
a0=a0,
|
||||
a1=a1,
|
||||
)
|
||||
if cc < cur_cc:
|
||||
cur, cur_cc = trial, cc
|
||||
improved = True
|
||||
break
|
||||
if improved:
|
||||
break
|
||||
if cur_cc < best_cc:
|
||||
best_cc = cur_cc
|
||||
best_ord = cur
|
||||
return best_ord, best_cc
|
||||
|
||||
|
||||
def _contracted_crossings_at_pos(
|
||||
hub: str,
|
||||
stubs: list[str],
|
||||
links: list[tuple[str, str]],
|
||||
pos: dict[str, tuple[float, float]],
|
||||
foreign_pos: dict[str, tuple[float, float]],
|
||||
) -> int:
|
||||
"""Contracted crossings using *actual* stub coordinates as representatives."""
|
||||
cpos: dict[str, tuple[float, float]] = {hub: pos[hub]}
|
||||
cpos.update(foreign_pos)
|
||||
for s in stubs:
|
||||
if s in pos:
|
||||
cpos[s] = pos[s]
|
||||
segs = [(a, b) for a, b in links if a in cpos and b in cpos]
|
||||
c = 0
|
||||
for i, (a, b) in enumerate(segs):
|
||||
p1, p2 = cpos[a], cpos[b]
|
||||
for u, v in segs[i + 1 :]:
|
||||
if len({a, b, u, v}) < 4:
|
||||
continue
|
||||
if segments_properly_intersect(p1, p2, cpos[u], cpos[v]):
|
||||
c += 1
|
||||
return c
|
||||
|
||||
|
||||
def _swap_territories(
|
||||
hub: str,
|
||||
a: str,
|
||||
b: str,
|
||||
owner: dict[str, str],
|
||||
pinned: set[str],
|
||||
pos: dict[str, tuple[float, float]],
|
||||
) -> dict[str, tuple[float, float]]:
|
||||
"""Exchange polar angles of two stub territories around hub (rigid)."""
|
||||
hx, hy = pos[hub]
|
||||
aa, ab = _ang(pos, hub, a), _ang(pos, hub, b)
|
||||
delta = ab - aa
|
||||
trial = dict(pos)
|
||||
ta = {n for n, o in owner.items() if o == a} | {a}
|
||||
tb = {n for n, o in owner.items() if o == b} | {b}
|
||||
for n in ta:
|
||||
if n not in pos or (n in pinned and n != a):
|
||||
continue
|
||||
x, y = pos[n]
|
||||
dx, dy = x - hx, y - hy
|
||||
r = math.hypot(dx, dy)
|
||||
th = math.atan2(dy, dx) + delta
|
||||
trial[n] = (hx + r * math.cos(th), hy + r * math.sin(th))
|
||||
for n in tb:
|
||||
if n not in pos or (n in pinned and n != b):
|
||||
continue
|
||||
x, y = pos[n]
|
||||
dx, dy = x - hx, y - hy
|
||||
r = math.hypot(dx, dy)
|
||||
th = math.atan2(dy, dx) - delta
|
||||
trial[n] = (hx + r * math.cos(th), hy + r * math.sin(th))
|
||||
trial[hub] = (hx, hy)
|
||||
return trial
|
||||
|
||||
|
||||
def _realize_order_by_swaps(
|
||||
hub: str,
|
||||
stubs: list[str],
|
||||
owner: dict[str, str],
|
||||
pinned: set[str],
|
||||
pos: dict[str, tuple[float, float]],
|
||||
clinks: list[tuple[str, str]],
|
||||
foreign: dict[str, tuple[float, float]],
|
||||
links: list[tuple[str, str]],
|
||||
*,
|
||||
target_ord: list[str] | None = None,
|
||||
allow_global_rise: bool = False,
|
||||
) -> tuple[dict[str, tuple[float, float]], int, int]:
|
||||
"""Greedy adjacent territory swaps; primary score = contracted@pos.
|
||||
|
||||
Search is driven by contracted crossings (lower-level edges ignored in
|
||||
the objective). On an already-laid canvas (stage-2 polish) we also refuse
|
||||
swaps that raise *global* crossings — otherwise leaf noise swamps the
|
||||
map before lower levels can be re-expanded. Set allow_global_rise for
|
||||
true build-from-skeleton top-down.
|
||||
"""
|
||||
trial = dict(pos)
|
||||
before = _contracted_crossings_at_pos(hub, stubs, clinks, trial, foreign)
|
||||
g_base = count_edge_crossings(trial, links)
|
||||
target_idx = {s: i for i, s in enumerate(target_ord)} if target_ord else {}
|
||||
|
||||
for _ in range(max(2 * len(stubs), 8)):
|
||||
geo = sorted(stubs, key=lambda s: _ang(trial, hub, s))
|
||||
pair_idxs = list(range(len(geo) - 1))
|
||||
if target_idx:
|
||||
inv = [
|
||||
i
|
||||
for i in pair_idxs
|
||||
if target_idx.get(geo[i], 0) > target_idx.get(geo[i + 1], 0)
|
||||
]
|
||||
pair_idxs = inv + [i for i in pair_idxs if i not in inv]
|
||||
progressed = False
|
||||
c0 = _contracted_crossings_at_pos(hub, stubs, clinks, trial, foreign)
|
||||
g0 = count_edge_crossings(trial, links)
|
||||
# Pick the adjacent swap with best contracted drop; tie-break by global.
|
||||
best_cand = None
|
||||
best_key = None
|
||||
for i in pair_idxs:
|
||||
cand = _swap_territories(hub, geo[i], geo[i + 1], owner, pinned, trial)
|
||||
for h in pinned:
|
||||
if h in pos:
|
||||
cand[h] = pos[h]
|
||||
cand[hub] = pos[hub]
|
||||
c1 = _contracted_crossings_at_pos(hub, stubs, clinks, cand, foreign)
|
||||
if c1 >= c0:
|
||||
continue
|
||||
g1 = count_edge_crossings(cand, links)
|
||||
if not allow_global_rise and g1 > g0:
|
||||
continue
|
||||
key = (c0 - c1, g0 - g1)
|
||||
if best_key is None or key > best_key:
|
||||
best_key = key
|
||||
best_cand = cand
|
||||
if best_cand is not None:
|
||||
trial = best_cand
|
||||
progressed = True
|
||||
if not progressed:
|
||||
break
|
||||
after = _contracted_crossings_at_pos(hub, stubs, clinks, trial, foreign)
|
||||
del g_base
|
||||
return trial, before, after
|
||||
|
||||
|
||||
def hierarchy_sectors_greedy(
|
||||
state: LayoutState,
|
||||
params: LayoutParams | None = None,
|
||||
*,
|
||||
step: float | None = None,
|
||||
max_hubs: int = 20,
|
||||
allow_global_rise: bool = False,
|
||||
) -> OpResult:
|
||||
"""Top-down: contract → order by contracted crossings → rigid expand.
|
||||
|
||||
Role beam offsets (pin_beam) belong *after* this pass, not inside it.
|
||||
Default stage-2 polish keeps global from rising; pass allow_global_rise
|
||||
for skeleton-time top-down where leaf crossings are deferred.
|
||||
"""
|
||||
params = params or LayoutParams()
|
||||
step_px = float(step if step is not None else max(params.pitch * 0.95, 170.0))
|
||||
pos = dict(state.positions)
|
||||
links = list(state.links)
|
||||
adj = state.adj
|
||||
global0 = count_edge_crossings(pos, links)
|
||||
|
||||
hubs = pick_hub_seeds(state, max_hubs=max_hubs)
|
||||
cores = [h for h in hubs if state.layers.get(h) == "core"]
|
||||
aggs = [h for h in hubs if state.layers.get(h) == "agg"]
|
||||
# Top-down: cores first, then aggs, then remaining high-degree hubs.
|
||||
order_hubs = list(cores) + [h for h in aggs if h not in cores]
|
||||
for h in hubs:
|
||||
if h not in order_hubs:
|
||||
order_hubs.append(h)
|
||||
|
||||
pinned = set(hubs)
|
||||
hub_of = _owner_hub_map(adj, pinned)
|
||||
level_notes: list[dict[str, Any]] = []
|
||||
any_level = False
|
||||
contracted_gain = 0
|
||||
|
||||
for hub in order_hubs:
|
||||
if hub not in pos:
|
||||
continue
|
||||
members = {hub}
|
||||
q: deque[str] = deque([hub])
|
||||
while q:
|
||||
u = q.popleft()
|
||||
for v in adj.get(u, ()):
|
||||
if v in members:
|
||||
continue
|
||||
if v in pinned and v != hub:
|
||||
continue
|
||||
members.add(v)
|
||||
q.append(v)
|
||||
for v in adj.get(hub, ()):
|
||||
if v not in pinned:
|
||||
members.add(v)
|
||||
|
||||
stubs, owner = _stub_territories(hub, members, adj, pinned)
|
||||
if len(stubs) < 2:
|
||||
continue
|
||||
|
||||
clinks = _contracted_links(
|
||||
hub, stubs, owner, adj, pinned, hub_of=hub_of
|
||||
)
|
||||
foreign = {
|
||||
n: pos[n]
|
||||
for n in pinned
|
||||
if n != hub and n in pos and any(n in e for e in clinks)
|
||||
}
|
||||
angs = [_ang(pos, hub, s) for s in stubs]
|
||||
a0, a1 = min(angs) - 0.15, max(angs) + 0.15
|
||||
if a1 - a0 < 0.8:
|
||||
mid = sum(angs) / len(angs)
|
||||
a0, a1 = mid - 1.1, mid + 1.1
|
||||
|
||||
geo = sorted(stubs, key=lambda s: _ang(pos, hub, s))
|
||||
base_cc = _score_order(
|
||||
hub,
|
||||
geo,
|
||||
clinks,
|
||||
radius=step_px,
|
||||
hub_xy=pos[hub],
|
||||
foreign_pos=foreign,
|
||||
a0=a0,
|
||||
a1=a1,
|
||||
)
|
||||
best_ord, best_cc = _best_order_2opt(
|
||||
hub,
|
||||
stubs,
|
||||
pos,
|
||||
clinks,
|
||||
radius=step_px,
|
||||
hub_xy=pos[hub],
|
||||
foreign_pos=foreign,
|
||||
a0=a0,
|
||||
a1=a1,
|
||||
)
|
||||
# Abstract arc bakeoff only proposes a target cyclic order.
|
||||
if best_cc >= base_cc:
|
||||
continue
|
||||
|
||||
# Realize via adjacent territory swaps; accept on contracted@pos only.
|
||||
trial, c_before, c_after = _realize_order_by_swaps(
|
||||
hub,
|
||||
stubs,
|
||||
owner,
|
||||
pinned,
|
||||
pos,
|
||||
clinks,
|
||||
foreign,
|
||||
links,
|
||||
target_ord=best_ord,
|
||||
allow_global_rise=allow_global_rise,
|
||||
)
|
||||
if c_after >= c_before:
|
||||
continue
|
||||
for h in pinned:
|
||||
if h in pos:
|
||||
trial[h] = pos[h]
|
||||
trial[hub] = pos[hub]
|
||||
# No per-level overlap fix — it fights sector moves and is slow on
|
||||
# large canvases. Zero-overlap is ensured by the layout_tool wrapper.
|
||||
g1 = count_edge_crossings(trial, links)
|
||||
pos = trial
|
||||
any_level = True
|
||||
contracted_gain += c_before - c_after
|
||||
level_notes.append(
|
||||
{
|
||||
"hub_id": hub,
|
||||
"hub_name": state.names.get(hub, hub),
|
||||
"stubs": len(stubs),
|
||||
"contracted_before": c_before,
|
||||
"contracted_after": c_after,
|
||||
"contracted_arc_before": base_cc,
|
||||
"contracted_arc_after": best_cc,
|
||||
"global_after": g1,
|
||||
}
|
||||
)
|
||||
|
||||
if not any_level:
|
||||
return OpResult(
|
||||
state=state,
|
||||
moved=set(),
|
||||
op="hierarchy_sectors",
|
||||
params={"accepted_n": 0, "contracted_gain": 0},
|
||||
note="no_level_improved",
|
||||
)
|
||||
|
||||
# One end-of-pass overlap polish (hubs stay pinned).
|
||||
st_end = state.copy()
|
||||
st_end.positions = pos
|
||||
st_end = fix_overlaps_local(st_end, params).state
|
||||
for h in pinned:
|
||||
if h in pos:
|
||||
st_end.positions[h] = pos[h]
|
||||
pos = {k: (float(v[0]), float(v[1])) for k, v in st_end.positions.items()}
|
||||
|
||||
out = state.copy()
|
||||
out.positions = pos
|
||||
moved = {
|
||||
n for n, p in pos.items() if n in state.positions and p != state.positions[n]
|
||||
}
|
||||
g_end = count_edge_crossings(pos, links)
|
||||
out.meta = dict(out.meta or {})
|
||||
out.meta["hierarchy_sectors"] = {
|
||||
"levels": level_notes,
|
||||
"crossings_before": global0,
|
||||
"crossings_after": g_end,
|
||||
"contracted_gain": contracted_gain,
|
||||
}
|
||||
return OpResult(
|
||||
state=out,
|
||||
moved=moved,
|
||||
op="hierarchy_sectors",
|
||||
params={
|
||||
"accepted_n": len(level_notes),
|
||||
"levels": level_notes[:12],
|
||||
"crossings_before": global0,
|
||||
"crossings_after": g_end,
|
||||
"contracted_gain": contracted_gain,
|
||||
},
|
||||
note=(
|
||||
f"hierarchy_sectors:{len(level_notes)} levels "
|
||||
f"contractedΔ={contracted_gain} x:{global0}->{g_end}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def hierarchy_params_from_overrides(overrides: dict[str, Any] | None) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
if not overrides:
|
||||
return out
|
||||
if overrides.get("step") is not None:
|
||||
try:
|
||||
out["step"] = float(overrides["step"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if overrides.get("max_hubs") is not None:
|
||||
try:
|
||||
out["max_hubs"] = int(overrides["max_hubs"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if "allow_global_rise" in overrides:
|
||||
out["allow_global_rise"] = bool(overrides.get("allow_global_rise"))
|
||||
return out
|
||||
Loading…
Add table
Add a link
Reference in a new issue