MHRN-Space / src /playground /builder /network_builder.py
ThomasHeisig's picture
Sync GitHub main e8a04aea24121d15707238534e635cf0ceee8eaf (part 8)
250836c verified
Raw History Blame Contribute Delete
3.64 kB
"""Composable topology builder for exploratory Playground use."""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from ..models import Topology
from ..registry.topology_generators import build_topology
def _as_int(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(f"{label} must be an integer")
return value
@dataclass(slots=True)
class TopologyBuilder:
"""Fluent builder for one bounded exploratory topology."""
n: int
seed: int = 12345
dimensions: int = 5
layers: list[dict[str, object]] = field(default_factory=list)
connections: list[dict[str, object]] = field(default_factory=list)
def add_layer(
self, name: str, dim: int, n: int, **options: object
) -> "TopologyBuilder":
self.layers.append({"name": name, "dim": dim, "n": n, **options})
return self
def connect(
self,
source: str,
target: str,
rule: str = "random",
budget: int = 128,
) -> "TopologyBuilder":
self.connections.append(
{"source": source, "target": target, "rule": rule, "budget": budget}
)
return self
def build(self) -> Topology:
if not 1 <= self.dimensions <= 32:
raise ValueError("builder dimensions must be between 1 and 32")
if not self.layers:
topology_name = "mhrn_5d" if self.dimensions == 5 else "generic_nd"
return build_topology(
topology_name,
self.n,
max(self.n, self.n * 4),
self.seed,
dimensions=self.dimensions,
)
if sum(_as_int(layer["n"], "layer n") for layer in self.layers) != self.n:
raise ValueError("layer neuron counts must sum to builder n")
rng = random.Random(self.seed)
coordinates: list[tuple[float, ...]] = []
offsets: dict[str, tuple[int, int]] = {}
cursor = 0
for layer_index, layer in enumerate(self.layers):
count = _as_int(layer["n"], "layer n")
dim = _as_int(layer["dim"], "layer dim")
offsets[str(layer["name"])] = (cursor, cursor + count)
for local in range(count):
point = [0.0 for _ in range(max(dim, self.dimensions))]
point[0] = local / max(count - 1, 1)
if len(point) > 1:
point[1] = layer_index / max(len(self.layers) - 1, 1)
coordinates.append(tuple(point[: self.dimensions]))
cursor += count
edges: set[tuple[int, int]] = set()
for spec in self.connections:
source_lo, source_hi = offsets[str(spec["source"])]
target_lo, target_hi = offsets[str(spec["target"])]
budget = _as_int(spec["budget"], "connection budget")
target_size = len(edges) + budget
attempts = 0
while len(edges) < target_size and attempts < budget * 50:
attempts += 1
source = rng.randrange(source_lo, source_hi)
target = rng.randrange(target_lo, target_hi)
if source != target:
edges.add((source, target))
return Topology(
"composed",
self.dimensions,
tuple(sorted(edges)),
tuple(coordinates),
{
"layers": self.layers,
"connections": self.connections,
"seed": self.seed,
"geometry_class": "composed_nd",
},
)