File size: 3,642 Bytes
250836c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""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",
            },
        )