Quazim0t0 commited on
Commit
45e7dfb
·
verified ·
1 Parent(s): 9f0e510

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. __pycache__/app.cpython-313.pyc +0 -0
  2. __pycache__/features.cpython-313.pyc +0 -0
  3. __pycache__/mm_grad.cpython-313.pyc +0 -0
  4. __pycache__/modular_mind.cpython-313.pyc +0 -0
  5. __pycache__/online.cpython-313.pyc +0 -0
  6. agents/modmind/config.py +158 -0
  7. agents/modmind/language/tokenizer.json +0 -0
  8. agents/modmind/model.py +1011 -0
  9. agents/modmind/moe_gradio.py +388 -0
  10. agents/modmind/reasoning/tokenizer.json +0 -0
  11. agents/modmind/registry.py +44 -0
  12. agents/modmind/specialist_presets.py +69 -0
  13. agents/modmind/spike_tokenizer.py +82 -0
  14. agents/panel.py +292 -0
  15. app.py +535 -0
  16. assets_data.js +0 -0
  17. audio/sfx/hurt3_monster.wav +0 -0
  18. audio/sfx/hurt_knight.wav +0 -0
  19. audio/sfx/hurt_monster.wav +0 -0
  20. audio/sfx/jump_knight.wav +0 -0
  21. audio/sfx/roar2_monster.wav +0 -0
  22. audio/sfx/roar3_monster.wav +0 -0
  23. audio/sfx/roar4_monster.wav +0 -0
  24. audio/sfx/roar5_monster.wav +0 -0
  25. audio/sfx/roar6_monster.wav +0 -0
  26. audio/sfx/roar_monster.wav +0 -0
  27. audio/sfx/walk_boss.wav +0 -0
  28. audio/sfx/walk_knight.wav +0 -0
  29. features.py +53 -0
  30. mm_grad.py +257 -0
  31. modular_mind.py +185 -0
  32. online.py +124 -0
  33. piano/notes.json +1 -0
  34. piano/piano_mind.py +168 -0
  35. piano/poly_mind.py +150 -0
  36. piano/poly_notes.json +1 -0
  37. piano/samples/48.mp3 +0 -0
  38. piano/samples/53.mp3 +0 -0
  39. piano/samples/60.mp3 +0 -0
  40. piano/samples/65.mp3 +0 -0
  41. piano/samples/69.mp3 +0 -0
  42. piano/samples/74.mp3 +0 -0
  43. piano/samples/79.mp3 +0 -0
  44. piano/samples/84.mp3 +0 -0
  45. piano/samples/89.mp3 +0 -0
  46. requirements.txt +4 -0
  47. web/README.md +168 -0
  48. web/game.css +108 -0
  49. web/game.js +653 -0
  50. web/index.html +48 -0
__pycache__/app.cpython-313.pyc ADDED
Binary file (31 kB). View file
 
__pycache__/features.cpython-313.pyc ADDED
Binary file (3 kB). View file
 
__pycache__/mm_grad.cpython-313.pyc ADDED
Binary file (17.3 kB). View file
 
__pycache__/modular_mind.cpython-313.pyc ADDED
Binary file (11.1 kB). View file
 
__pycache__/online.cpython-313.pyc ADDED
Binary file (7.53 kB). View file
 
agents/modmind/config.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ config.py -- SpikeWhale: combined config from SpikeTransformer (My Project) + NanoWhale (DeepSeek-V4).
3
+
4
+ Features carried from My Project (not in NanoWhale):
5
+ - DERF attention: erf(alpha*score+bias)*gamma replaces softmax
6
+ - XSA (Exclusive Self-Attention): orthogonality correction removes self-echo from attn output
7
+ - Engram N-gram module: hash-table N-gram lookup with DERF gate injected into embeddings
8
+ - Three-tier optimizer: embed/table params trained at lower LR
9
+
10
+ Features carried from NanoWhale (not in My Project):
11
+ - MLA (Multi-Head Latent Attention): low-rank Q projection + direct K,V (MQA)
12
+ - Partial RoPE: rotary embeddings on only qk_rope_head_dim dims of Q and K
13
+ - Low-rank grouped output projection (o_lora_rank)
14
+ - Hyper-Connections: hc_mult residual streams with learned routing between layers
15
+ - Shared expert in MoE (always-active expert alongside routed experts)
16
+ - sqrtsoftplus expert scoring (vs softmax in My Project)
17
+ - Hash-based routing for first num_hash_layers layers
18
+ - norm_topk_prob + routed_scaling_factor
19
+ - Multi-Token Prediction (MTP): extra heads predict k steps ahead
20
+ - torch.compile, FineWeb-Edu streaming, Trackio, YAML configs in train.py
21
+ """
22
+
23
+ from transformers import PretrainedConfig
24
+
25
+
26
+ class SpikeWhaleConfig(PretrainedConfig):
27
+ model_type = "spike_whale"
28
+
29
+ def __init__(
30
+ self,
31
+ # Standard
32
+ vocab_size: int = 129280,
33
+ hidden_size: int = 2048,
34
+ num_hidden_layers: int = 11,
35
+ max_position_embeddings: int = 8192,
36
+ rms_norm_eps: float = 1e-6,
37
+ initializer_range: float = 0.02,
38
+ tie_word_embeddings: bool = False,
39
+ hidden_dropout: float = 0.0,
40
+ bos_token_id: int = 0,
41
+ eos_token_id: int = 1,
42
+ # MLA Attention (NanoWhale)
43
+ num_attention_heads: int = 8,
44
+ num_key_value_heads: int = 1, # 1 = MQA; >1 = GQA
45
+ q_lora_rank: int = 160, # low-rank Q: hidden -> q_lora_rank -> num_heads*head_dim
46
+ head_dim: int = 96, # total per-head dim = nope_head_dim + qk_rope_head_dim
47
+ qk_rope_head_dim: int = 32, # RoPE applied only to these dims
48
+ o_lora_rank: int = 80, # low-rank output: num_heads*head_dim -> o_lora_rank -> hidden
49
+ attention_dropout: float = 0.0,
50
+ rope_theta: float = 10000.0,
51
+ # DERF + XSA (My Project)
52
+ use_derf: bool = True,
53
+ use_xsa: bool = True,
54
+ # MoE (combined)
55
+ use_moe: bool = True,
56
+ moe_intermediate_size: int = 640,
57
+ n_routed_experts: int = 4,
58
+ n_shared_experts: int = 1, # NanoWhale: always-active shared expert
59
+ num_experts_per_tok: int = 2,
60
+ norm_topk_prob: bool = True, # NanoWhale: normalize top-k routing weights
61
+ scoring_func: str = "sqrtsoftplus", # NanoWhale: sqrt(softplus(x)) vs softmax
62
+ routed_scaling_factor: float = 1.0, # NanoWhale: scale routed expert weights
63
+ num_hash_layers: int = 2, # NanoWhale: first N layers use hash routing
64
+ moe_aux_loss_coef: float = 0.01,
65
+ moe_layers: list = None,
66
+ # Hyper-Connections (NanoWhale)
67
+ use_hyper_connections: bool = True,
68
+ hc_mult: int = 4, # number of parallel residual streams
69
+ hc_sinkhorn_iters: int = 20,
70
+ hc_eps: float = 1e-6,
71
+ # Multi-Token Prediction (NanoWhale)
72
+ num_nextn_predict_layers: int = 1, # extra MTP heads (0 = disabled)
73
+ # Engram N-gram module (My Project)
74
+ use_engram: bool = True,
75
+ engram_compress_dim: int = 64,
76
+ engram_num_heads: int = 4,
77
+ engram_table_size: int = 8192,
78
+ engram_max_ngram: int = 3,
79
+ engram_gate_init_bias: float = -4.0,
80
+ use_hrm_refine: bool = False,
81
+ hrm_refine_steps: int = 3,
82
+ hrm_refine_dim: int = 256,
83
+ # --- ModularMind-on-V2 additions (off/unused unless enabled) ---
84
+ use_latent_io: bool = False, # add latent output head + injection input path
85
+ d_latent: int = 256, # RecursiveLink contract dim (fixed across chain)
86
+ chain_position: int = 0, # context-doubling slot: ctx & theta scale by 2^pos
87
+ base_context: int = 8192, # ctx at position 0 (>= training --seq-len)
88
+ base_rope_theta: float = 10000.0,
89
+ **kwargs,
90
+ ):
91
+ super().__init__(
92
+ bos_token_id=bos_token_id,
93
+ eos_token_id=eos_token_id,
94
+ tie_word_embeddings=tie_word_embeddings,
95
+ **kwargs,
96
+ )
97
+ self.vocab_size = vocab_size
98
+ self.hidden_size = hidden_size
99
+ self.num_hidden_layers = num_hidden_layers
100
+ self.max_position_embeddings = max_position_embeddings
101
+ self.rms_norm_eps = rms_norm_eps
102
+ self.initializer_range = initializer_range
103
+ self.hidden_dropout = hidden_dropout
104
+
105
+ self.num_attention_heads = num_attention_heads
106
+ self.num_key_value_heads = num_key_value_heads
107
+ self.q_lora_rank = q_lora_rank
108
+ self.head_dim = head_dim
109
+ self.qk_rope_head_dim = qk_rope_head_dim
110
+ self.nope_head_dim = head_dim - qk_rope_head_dim
111
+ self.o_lora_rank = o_lora_rank
112
+ self.attention_dropout = attention_dropout
113
+ self.rope_theta = rope_theta
114
+ self.use_derf = use_derf
115
+ self.use_xsa = use_xsa
116
+
117
+ self.use_moe = use_moe
118
+ self.moe_intermediate_size = moe_intermediate_size
119
+ self.n_routed_experts = n_routed_experts
120
+ self.n_shared_experts = n_shared_experts
121
+ self.num_experts_per_tok = num_experts_per_tok
122
+ self.norm_topk_prob = norm_topk_prob
123
+ self.scoring_func = scoring_func
124
+ self.routed_scaling_factor = routed_scaling_factor
125
+ self.num_hash_layers = num_hash_layers
126
+ self.moe_aux_loss_coef = moe_aux_loss_coef
127
+ self.moe_layers = moe_layers if moe_layers is not None else list(range(num_hidden_layers))
128
+
129
+ self.use_hyper_connections = use_hyper_connections
130
+ self.hc_mult = hc_mult
131
+ self.hc_sinkhorn_iters = hc_sinkhorn_iters
132
+ self.hc_eps = hc_eps
133
+
134
+ self.num_nextn_predict_layers = num_nextn_predict_layers
135
+
136
+ self.use_engram = use_engram
137
+ self.engram_compress_dim = engram_compress_dim
138
+ self.engram_num_heads = engram_num_heads
139
+ self.engram_table_size = engram_table_size
140
+ self.engram_max_ngram = engram_max_ngram
141
+ self.engram_gate_init_bias = engram_gate_init_bias
142
+ self.use_hrm_refine = use_hrm_refine
143
+ self.hrm_refine_steps = hrm_refine_steps
144
+ self.hrm_refine_dim = hrm_refine_dim
145
+ # --- ModularMind-on-V2 additions ---
146
+ self.use_latent_io = use_latent_io
147
+ self.d_latent = d_latent
148
+ self.chain_position = chain_position
149
+ self.base_context = base_context
150
+ self.base_rope_theta = base_rope_theta
151
+ # Context-doubling: each chain slot doubles ctx and rope theta.
152
+ # position 0 -> (8192, 10000); position 1 -> (16384, 20000); etc.
153
+ # Applied only when latent IO is on (i.e. this is a ModularMind specialist),
154
+ # so plain V2 keeps its own max_position_embeddings/rope_theta untouched.
155
+ if use_latent_io:
156
+ scale = 2 ** chain_position
157
+ self.max_position_embeddings = base_context * scale
158
+ self.rope_theta = base_rope_theta * scale
agents/modmind/language/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
agents/modmind/model.py ADDED
@@ -0,0 +1,1011 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ model.py -- SpikeWhaleLM: combined architecture from SpikeTransformer (My Project) + NanoWhale.
3
+
4
+ Architecture flow:
5
+ Embedding
6
+ -> Engram delta (N-gram memory, My Project)
7
+ -> [expand to hc_mult copies if HC enabled]
8
+ -> N x TransformerBlock:
9
+ HC pre-op (NanoWhale) -> RMSNorm -> MLA+DERF+XSA Attention (combined)
10
+ -> HC post-op
11
+ HC pre-op -> RMSNorm -> MoE FFN w/ shared expert (NanoWhale)
12
+ -> HC post-op
13
+ -> [mean-pool hc_mult copies if HC enabled]
14
+ -> RMSNorm
15
+ -> LM head + MTP heads (NanoWhale)
16
+
17
+ Component origins:
18
+ RMSNorm, RotaryEmbedding -- both (standard)
19
+ Engram / DERFContextGate -- My Project
20
+ MLADerfXSAAttention -- MLA from NanoWhale + DERF+XSA from My Project
21
+ SparseMoEFFN w/ shared expert -- NanoWhale MoE structure + My Project aux loss
22
+ HyperConnectionLayer -- NanoWhale
23
+ SpikeWhaleLM + MTP heads -- NanoWhale
24
+ """
25
+
26
+ import math
27
+ import torch
28
+ import torch.nn as nn
29
+ import torch.nn.functional as F
30
+ from typing import Optional, Tuple, List
31
+ from transformers import PreTrainedModel
32
+ from transformers.modeling_outputs import CausalLMOutputWithPast
33
+ from torch.utils.checkpoint import checkpoint as gradient_checkpoint
34
+
35
+ from config import SpikeWhaleConfig
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Primitives
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class RMSNorm(nn.Module):
43
+ def __init__(self, dim: int, eps: float = 1e-6):
44
+ super().__init__()
45
+ self.eps = eps
46
+ self.weight = nn.Parameter(torch.ones(dim))
47
+
48
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
49
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
50
+
51
+
52
+ class RotaryEmbedding(nn.Module):
53
+ """RoPE for the rope partition of Q and K (qk_rope_head_dim dims only)."""
54
+
55
+ def __init__(self, dim: int, max_positions: int = 4096, theta: float = 10000.0):
56
+ super().__init__()
57
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
58
+ self.register_buffer("inv_freq", inv_freq)
59
+ t = torch.arange(max_positions).float()
60
+ freqs = torch.outer(t, inv_freq)
61
+ self.register_buffer("cos_cache", freqs.cos())
62
+ self.register_buffer("sin_cache", freqs.sin())
63
+
64
+ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> torch.Tensor:
65
+ """
66
+ x: [B, H, S, rope_dim]
67
+ position_ids: [B, S]
68
+ """
69
+ cos = self.cos_cache[position_ids].unsqueeze(1) # [B, 1, S, rope_dim//2]
70
+ sin = self.sin_cache[position_ids].unsqueeze(1)
71
+ d = cos.shape[-1]
72
+ x1, x2 = x[..., :d], x[..., d:]
73
+ return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Engram: N-gram hash lookup + DERF gate (My Project, preserved)
78
+ # ---------------------------------------------------------------------------
79
+
80
+ class TokenCompressor(nn.Module):
81
+ def __init__(self, embed_dim: int, compress_dim: int):
82
+ super().__init__()
83
+ self.proj = nn.Linear(embed_dim, compress_dim, bias=False)
84
+ nn.init.normal_(self.proj.weight, std=0.02)
85
+ # BUGFIX: this projection feeds ONLY the integer hash index
86
+ # (idx = h.abs().long() % table_size) in MultiHeadHashLookup. The .long()
87
+ # cast is non-differentiable, so no gradient ever reaches this weight --
88
+ # it can never learn. Worse, _classify_params put it in the weight-decay
89
+ # group, so AdamW was steadily shrinking it toward zero and degrading the
90
+ # hash projection over a long run. Freeze it: a fixed random projection is
91
+ # exactly the right behavior for an LSH-style hash, and freezing drops it
92
+ # from the optimizer (saves state) and from weight decay. Checkpoint-safe:
93
+ # the parameter still exists and is still saved/loaded in state_dict.
94
+ self.proj.weight.requires_grad_(False)
95
+
96
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
97
+ return self.proj(x)
98
+
99
+
100
+ class MultiHeadHashLookup(nn.Module):
101
+ def __init__(self, num_heads: int, table_size: int,
102
+ compress_dim: int, out_dim: int, max_ngram: int = 3):
103
+ super().__init__()
104
+ self.num_heads = num_heads
105
+ self.table_size = table_size
106
+ self.max_ngram = max_ngram
107
+ self.out_dim = out_dim
108
+
109
+ self.tables = nn.ModuleList([
110
+ nn.Embedding(table_size, out_dim) for _ in range(num_heads)
111
+ ])
112
+ for t in self.tables:
113
+ nn.init.normal_(t.weight, std=0.01)
114
+
115
+ for n in range(1, max_ngram + 1):
116
+ for k in range(n):
117
+ proj = torch.randn(num_heads, compress_dim)
118
+ proj = proj / (proj.norm(dim=1, keepdim=True) + 1e-8)
119
+ self.register_buffer(f"hash_proj_n{n}_p{k}", proj)
120
+
121
+ def forward(self, compressed: torch.Tensor) -> torch.Tensor:
122
+ """
123
+ compressed: [B, S, compress_dim]
124
+ returns: [B, S, out_dim]
125
+
126
+ All positions are processed in parallel. The outer loop runs max_ngram
127
+ times (≤3), not S times (≤2048). Each iteration is a single matmul +
128
+ embedding lookup across the whole sequence, making this GPU-friendly
129
+ and compatible with torch.compile.
130
+ """
131
+ B, S, _ = compressed.shape
132
+ device = compressed.device
133
+ out = torch.zeros(B, S, self.out_dim, device=device, dtype=compressed.dtype)
134
+ # Per-position normalization: tracks how many (n-gram × head) contributions
135
+ # each position receives. Positions near the start get fewer contributions
136
+ # because shorter n-grams don't exist yet (matches original causal behavior).
137
+ norm = torch.zeros(S, device=device)
138
+
139
+ for n in range(1, self.max_ngram + 1):
140
+ if S < n:
141
+ continue
142
+ valid_len = S - n + 1 # positions [n-1 .. S-1] are valid for order-n
143
+ start = n - 1
144
+
145
+ # Accumulate position-k contribution to the order-n hash.
146
+ # compressed[:, k : k+valid_len, :] is the k-th token of every n-gram
147
+ # window simultaneously → [B, valid_len, num_heads] after projection.
148
+ h = torch.zeros(B, valid_len, self.num_heads, device=device)
149
+ for k in range(n):
150
+ proj = getattr(self, f"hash_proj_n{n}_p{k}") # [num_heads, compress_dim]
151
+ h = h + torch.matmul(compressed[:, k:k + valid_len, :].float(), proj.t())
152
+
153
+ idx = h.abs().long() % self.table_size # [B, valid_len, num_heads]
154
+
155
+ for head_idx, table in enumerate(self.tables):
156
+ out[:, start:, :] = out[:, start:, :] + table(idx[:, :, head_idx])
157
+
158
+ norm[start:] += self.num_heads
159
+
160
+ # Cast back to input dtype: the norm division promotes bf16→float32 under autocast.
161
+ # Keeping the output in the same dtype as the input avoids a silent dtype mismatch
162
+ # when EngramModule adds this result back onto the (bf16) embedding tensor.
163
+ return (out / norm.view(1, -1, 1).clamp(min=1)).to(compressed.dtype)
164
+
165
+
166
+ class DERFContextGate(nn.Module):
167
+ """
168
+ DERF gate: gate = gamma * erf(alpha * proj([retrieved, x]) + bias)
169
+ Positive probability = (gate + 1) / 2 applied to retrieved embedding.
170
+ Large negative init_bias keeps gate closed at start of training.
171
+ """
172
+ def __init__(self, obs_size: int, init_bias: float = -4.0):
173
+ super().__init__()
174
+ self.proj = nn.Linear(obs_size * 2, obs_size)
175
+ self.alpha = nn.Parameter(torch.ones(obs_size))
176
+ self.bias = nn.Parameter(torch.full((obs_size,), init_bias))
177
+ self.gamma = nn.Parameter(torch.ones(obs_size))
178
+
179
+ def forward(self, retrieved: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
180
+ logits = self.proj(torch.cat([retrieved, x], dim=-1))
181
+ gate = self.gamma * ((torch.erf(self.alpha * logits + self.bias) + 1.0) / 2.0)
182
+ return retrieved * gate
183
+
184
+
185
+ class EngramModule(nn.Module):
186
+ """
187
+ N-gram hash lookup with DERF gate (My Project), fully vectorized.
188
+ All S positions are processed in parallel — the sequential Python loop
189
+ over sequence positions has been eliminated. The lookup now accepts the
190
+ full [B, S, compress_dim] compressed tensor and returns [B, S, H] in one pass.
191
+ """
192
+ def __init__(self, cfg: SpikeWhaleConfig):
193
+ super().__init__()
194
+ self.compressor = TokenCompressor(cfg.hidden_size, cfg.engram_compress_dim)
195
+ self.lookup = MultiHeadHashLookup(
196
+ cfg.engram_num_heads, cfg.engram_table_size,
197
+ cfg.engram_compress_dim, cfg.hidden_size, cfg.engram_max_ngram,
198
+ )
199
+ self.gate = DERFContextGate(cfg.hidden_size, cfg.engram_gate_init_bias)
200
+
201
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
202
+ """x: [B, S, H] -> engram_delta: [B, S, H]"""
203
+ compressed = self.compressor(x.detach()) # [B, S, compress_dim]
204
+ retrieved = self.lookup(compressed) # [B, S, H]
205
+ return self.gate(retrieved, x) # [B, S, H]
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Hyper-Connections (NanoWhale, simplified)
210
+ # ---------------------------------------------------------------------------
211
+
212
+ class HyperConnectionLayer(nn.Module):
213
+ """
214
+ Simplified Hyper-Connections for one sublayer (attention or FFN).
215
+
216
+ Maintains hc_mult parallel residual streams.
217
+ Pre-op: learned weighted average of hc_mult copies -> single hidden state for sublayer.
218
+ Post-op: sublayer output added to each copy with learned per-stream weights.
219
+
220
+ Full HC uses Sinkhorn-normalized 2D routing matrices; this uses softmax-normalized
221
+ 1D weights for pre/post routing -- captures the same multi-stream routing spirit.
222
+ """
223
+ def __init__(self, hidden_size: int, hc_mult: int,
224
+ sinkhorn_iters: int = 20, eps: float = 1e-6):
225
+ super().__init__()
226
+ self.hc_mult = hc_mult
227
+ # pre_weight: how to mix hc_mult copies into one sublayer input
228
+ # post_weight: how to distribute the sublayer delta to each copy
229
+ #
230
+ # BUGFIX: these must NOT be initialized identically across streams.
231
+ # The model expands the hidden state into hc_mult *identical* copies.
232
+ # With uniform pre/post weights, pre_op produces sum_i copy_i * w_i =
233
+ # copy * sum(softmax)=copy (all copies equal), and post_op adds the same
234
+ # delta to every copy -- so the streams stay byte-for-byte identical at
235
+ # every layer. When all streams are equal, the softmax Jacobian applied
236
+ # to the (equal) per-stream gradients is exactly zero, so pre_weight and
237
+ # post_weight receive ZERO gradient and never move off 1/hc_mult. The HC
238
+ # routing then learns nothing and just burns hc_mult x memory/compute.
239
+ #
240
+ # Breaking the post_weight symmetry at init makes the streams diverge
241
+ # after the first sublayer, which restores gradient flow to all HC
242
+ # weights. We center post_weight so softmax starts near-uniform (keeps
243
+ # the residual baseline ~unchanged) but with a distinct value per stream.
244
+ self.pre_weight = nn.Parameter(
245
+ torch.linspace(0.5, -0.5, hc_mult) / max(hc_mult, 1)
246
+ )
247
+ self.post_weight = nn.Parameter(
248
+ torch.linspace(-0.5, 0.5, hc_mult) / max(hc_mult, 1)
249
+ )
250
+
251
+ def pre_op(self, copies: torch.Tensor) -> torch.Tensor:
252
+ """copies: [B, hc_mult, S, H] -> [B, S, H]"""
253
+ w = F.softmax(self.pre_weight, dim=0) # [hc_mult]
254
+ return (copies * w.view(1, -1, 1, 1)).sum(dim=1)
255
+
256
+ def post_op(self, copies: torch.Tensor, delta: torch.Tensor) -> torch.Tensor:
257
+ """
258
+ copies: [B, hc_mult, S, H]
259
+ delta: [B, S, H]
260
+ Returns updated copies: [B, hc_mult, S, H]
261
+ """
262
+ w = F.softmax(self.post_weight, dim=0) # [hc_mult]
263
+ return copies + delta.unsqueeze(1) * w.view(1, -1, 1, 1)
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # MLA + DERF + XSA Attention (combined)
268
+ # ---------------------------------------------------------------------------
269
+
270
+ class MLADerfXSAAttention(nn.Module):
271
+ """
272
+ Multi-Head Latent Attention (NanoWhale) with DERF scores + XSA correction (My Project).
273
+
274
+ MLA (from NanoWhale):
275
+ Q: hidden -> q_lora_rank (RMSNorm) -> num_heads * head_dim (low-rank projection)
276
+ K, V: hidden -> num_kv_heads * head_dim (direct, MQA by default with num_kv_heads=1)
277
+ Output: num_heads * head_dim -> o_lora_rank -> hidden (low-rank output)
278
+ Partial RoPE: applied only to the last qk_rope_head_dim dims of Q and K
279
+
280
+ DERF (from My Project):
281
+ Replaces softmax: erf(alpha * scores + bias) * gamma, shifted to [0,1] then normalized.
282
+ Per-head learnable alpha, bias, gamma.
283
+
284
+ XSA (from My Project):
285
+ After computing the weighted value sum y, subtract the component of y that
286
+ projects onto each position's own value vector. Forces the output to carry
287
+ only cross-position information, not echo the current token back.
288
+ """
289
+
290
+ def __init__(self, cfg: SpikeWhaleConfig):
291
+ super().__init__()
292
+ self.num_heads = cfg.num_attention_heads
293
+ self.num_kv_heads = cfg.num_key_value_heads
294
+ self.head_dim = cfg.head_dim
295
+ self.qk_rope_head_dim = cfg.qk_rope_head_dim
296
+ self.nope_head_dim = cfg.nope_head_dim
297
+ self.hidden_size = cfg.hidden_size
298
+ self.use_derf = cfg.use_derf
299
+ self.use_xsa = cfg.use_xsa
300
+ self.dropout_p = cfg.attention_dropout
301
+ self.kv_groups = self.num_heads // self.num_kv_heads
302
+
303
+ # Low-rank Q projection (MLA)
304
+ self.q_a_proj = nn.Linear(cfg.hidden_size, cfg.q_lora_rank, bias=False)
305
+ self.q_a_norm = RMSNorm(cfg.q_lora_rank, cfg.rms_norm_eps)
306
+ self.q_b_proj = nn.Linear(cfg.q_lora_rank, self.num_heads * self.head_dim, bias=False)
307
+
308
+ # Direct K, V projections (MQA/GQA)
309
+ self.k_proj = nn.Linear(cfg.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
310
+ self.v_proj = nn.Linear(cfg.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
311
+
312
+ # Low-rank output projection (MLA)
313
+ self.o_a_proj = nn.Linear(self.num_heads * self.head_dim, cfg.o_lora_rank, bias=False)
314
+ self.o_b_proj = nn.Linear(cfg.o_lora_rank, cfg.hidden_size, bias=False)
315
+
316
+ # Partial RoPE: applied to qk_rope_head_dim dims only
317
+ self.rope = RotaryEmbedding(
318
+ self.qk_rope_head_dim,
319
+ max_positions=cfg.max_position_embeddings,
320
+ theta=cfg.rope_theta,
321
+ )
322
+
323
+ # DERF parameters: one per query head (My Project)
324
+ if self.use_derf:
325
+ self.derf_alpha = nn.Parameter(torch.ones(self.num_heads))
326
+ self.derf_bias = nn.Parameter(torch.zeros(self.num_heads))
327
+ self.derf_gamma = nn.Parameter(torch.ones(self.num_heads))
328
+
329
+ nn.init.normal_(self.q_a_proj.weight, std=cfg.initializer_range)
330
+ nn.init.normal_(self.q_b_proj.weight, std=cfg.initializer_range)
331
+ nn.init.normal_(self.k_proj.weight, std=cfg.initializer_range)
332
+ nn.init.normal_(self.v_proj.weight, std=cfg.initializer_range)
333
+ nn.init.normal_(self.o_a_proj.weight, std=cfg.initializer_range)
334
+ nn.init.normal_(self.o_b_proj.weight, std=cfg.initializer_range)
335
+
336
+ def forward(
337
+ self,
338
+ x: torch.Tensor,
339
+ position_ids: torch.Tensor,
340
+ attention_mask: Optional[torch.Tensor] = None,
341
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
342
+ use_cache: bool = False,
343
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
344
+ B, S, _ = x.shape
345
+
346
+ # Q via low-rank projection with intermediate norm (MLA)
347
+ q = self.q_a_norm(self.q_a_proj(x))
348
+ q = self.q_b_proj(q).view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
349
+ # [B, num_heads, S, head_dim]
350
+
351
+ # K, V direct projections
352
+ k = self.k_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
353
+ v = self.v_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
354
+
355
+ # Partial RoPE: split into nope and rope partitions, rotate only the rope part
356
+ q_nope = q[..., :self.nope_head_dim]
357
+ q_rope = q[..., self.nope_head_dim:] # qk_rope_head_dim dims
358
+ k_nope = k[..., :self.nope_head_dim]
359
+ k_rope = k[..., self.nope_head_dim:]
360
+
361
+ q_rope = self.rope(q_rope, position_ids)
362
+ k_rope = self.rope(k_rope, position_ids)
363
+
364
+ q = torch.cat([q_nope, q_rope], dim=-1)
365
+ k = torch.cat([k_nope, k_rope], dim=-1)
366
+
367
+ # KV cache for inference
368
+ if past_key_value is not None:
369
+ k = torch.cat([past_key_value[0], k], dim=2)
370
+ v = torch.cat([past_key_value[1], v], dim=2)
371
+ present = (k, v) if use_cache else None
372
+ N = k.shape[2] # total key positions (past + current)
373
+
374
+ # Expand KV heads for MQA/GQA
375
+ if self.kv_groups > 1:
376
+ k = k.unsqueeze(2).expand(-1, -1, self.kv_groups, -1, -1).reshape(
377
+ B, self.num_heads, N, self.head_dim)
378
+ v = v.unsqueeze(2).expand(-1, -1, self.kv_groups, -1, -1).reshape(
379
+ B, self.num_heads, N, self.head_dim)
380
+
381
+ # Scaled dot-product attention.
382
+ if self.use_derf:
383
+ # DERF replaces softmax with a custom erf nonlinearity, so it cannot
384
+ # use the fused kernel and must materialize scores explicitly.
385
+ scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
386
+
387
+ # Build boolean mask for causality (this avoids the -inf math errors)
388
+ if attention_mask is None and past_key_value is None:
389
+ is_masked = torch.triu(torch.ones(S, N, dtype=torch.bool, device=scores.device), diagonal=N - S + 1).unsqueeze(0).unsqueeze(0)
390
+ else:
391
+ is_masked = (attention_mask < -1.0) if attention_mask is not None else torch.zeros_like(scores, dtype=torch.bool)
392
+
393
+ # FIX 2: Do NOT use float('-inf'). If alpha ever hits 0.0, 0.0 * -inf = NaN.
394
+ # Use a safe negative scalar (-10000.0) for masked positions.
395
+ safe_scores = scores.masked_fill(is_masked, -10000.0)
396
+
397
+ a = self.derf_alpha.view(1, -1, 1, 1)
398
+ b = self.derf_bias.view(1, -1, 1, 1)
399
+ g = self.derf_gamma.view(1, -1, 1, 1)
400
+
401
+ attn_weights = g * torch.erf(a * safe_scores + b) # [-gamma, gamma]
402
+ attn_weights = (attn_weights + g) / 2.0 # shift to [0, gamma]
403
+ attn_weights = attn_weights.masked_fill(is_masked, 0.0) # enforce causal mask safely
404
+ attn_weights = attn_weights / (attn_weights.sum(dim=-1, keepdim=True) + 1e-8)
405
+
406
+ if self.dropout_p > 0 and self.training:
407
+ attn_weights = F.dropout(attn_weights, p=self.dropout_p)
408
+
409
+ y = torch.matmul(attn_weights, v) # [B, num_heads, S, head_dim]
410
+ else:
411
+ # OPTIMIZATION: standard (softmax) attention goes through the fused
412
+ # scaled_dot_product_attention kernel (FlashAttention / mem-efficient
413
+ # backends). This is the hot path during pretraining (use_derf=False)
414
+ # and is much faster + lower memory than materializing [B,H,S,N]
415
+ # scores and a softmax. SDPA already scales by 1/sqrt(head_dim).
416
+ #
417
+ # CONTIGUITY FIX: with MQA/GQA, k and v above are built via
418
+ # .unsqueeze(2).expand(...).reshape(...). Under torch.compile, inductor
419
+ # can trace the broadcasted (zero-stride) view through to the fused
420
+ # flash-attention BACKWARD kernel, whose meta-kernel then asserts on the
421
+ # mismatched stride (e.g. "stride 120==245760 at dim=1") and aborts.
422
+ # Forcing contiguity guarantees standard strides into the fused kernel.
423
+ q = q.contiguous()
424
+ k = k.contiguous()
425
+ v = v.contiguous()
426
+ drop = self.dropout_p if self.training else 0.0
427
+ if past_key_value is None and attention_mask is None:
428
+ # Prefill / training: pure causal mask, no materialization needed.
429
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True, dropout_p=drop)
430
+ else:
431
+ # Incremental decode or a provided mask: pass an explicit boolean
432
+ # keep-mask (True = attend). SDPA fills masked positions with -inf.
433
+ if attention_mask is not None:
434
+ is_masked = (attention_mask < -1.0)
435
+ else:
436
+ is_masked = torch.triu(
437
+ torch.ones(S, N, dtype=torch.bool, device=q.device),
438
+ diagonal=N - S + 1,
439
+ ).unsqueeze(0).unsqueeze(0)
440
+ y = F.scaled_dot_product_attention(
441
+ q, k, v, attn_mask=~is_masked, dropout_p=drop)
442
+
443
+ # XSA: remove self-projection from output (My Project)
444
+ # For each query position s, subtract the component of y[:,:,s,:] that
445
+ # projects onto the normalized value vector at the same position.
446
+ if self.use_xsa:
447
+ past_len = N - S
448
+ v_self = v[:, :, past_len:past_len + S, :] # [B, H, S, D]
449
+ vn = v_self / (v_self.norm(dim=-1, keepdim=True) + 1e-8)
450
+ projection = (y * vn).sum(dim=-1, keepdim=True) * vn
451
+ y = y - projection
452
+
453
+ # Low-rank output projection (MLA)
454
+ y = y.transpose(1, 2).contiguous().view(B, S, self.num_heads * self.head_dim)
455
+ y = self.o_b_proj(self.o_a_proj(y))
456
+ return y, present
457
+
458
+
459
+ # ---------------------------------------------------------------------------
460
+ # MoE FFN: shared expert + sqrtsoftplus + hash routing (NanoWhale) + aux loss (My Project)
461
+ # ---------------------------------------------------------------------------
462
+
463
+ class ExpertFFN(nn.Module):
464
+ """Single SwiGLU expert."""
465
+ def __init__(self, hidden_size: int, intermediate_size: int):
466
+ super().__init__()
467
+ self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
468
+ self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
469
+ self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
470
+
471
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
472
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
473
+
474
+
475
+ def sqrtsoftplus(x: torch.Tensor) -> torch.Tensor:
476
+ """sqrt(softplus(x)) = sqrt(log(1+exp(x))). NanoWhale expert scoring."""
477
+ # FIX 1: Added 1e-8. If F.softplus(x) evaluates to 0.0, torch.sqrt(0) produces NaN gradients on backward pass.
478
+ return torch.sqrt(F.softplus(x) + 1e-8)
479
+
480
+
481
+ class SparseMoEFFN(nn.Module):
482
+ """
483
+ Combines NanoWhale MoE structure with My Project aux loss:
484
+ - n_shared_experts always-active experts (NanoWhale)
485
+ - n_routed_experts sparse routed experts, top-k activation
486
+ - sqrtsoftplus scoring (NanoWhale) vs softmax
487
+ - hash routing for early layers (NanoWhale)
488
+ - norm_topk_prob + routed_scaling_factor (NanoWhale)
489
+ - load-balancing aux loss (My Project)
490
+ """
491
+ def __init__(self, cfg: SpikeWhaleConfig, layer_idx: int = 0):
492
+ super().__init__()
493
+ self.n_routed_experts = cfg.n_routed_experts
494
+ self.n_shared_experts = cfg.n_shared_experts
495
+ self.num_experts_per_tok = cfg.num_experts_per_tok
496
+ self.norm_topk_prob = cfg.norm_topk_prob
497
+ self.scoring_func = cfg.scoring_func
498
+ self.routed_scaling_factor = cfg.routed_scaling_factor
499
+ self.use_hash_routing = layer_idx < cfg.num_hash_layers
500
+ self.aux_loss_coef = cfg.moe_aux_loss_coef
501
+
502
+ self.router = nn.Linear(cfg.hidden_size, cfg.n_routed_experts, bias=False)
503
+ self.experts = nn.ModuleList([
504
+ ExpertFFN(cfg.hidden_size, cfg.moe_intermediate_size)
505
+ for _ in range(cfg.n_routed_experts)
506
+ ])
507
+ self.shared_experts = nn.ModuleList([
508
+ ExpertFFN(cfg.hidden_size, cfg.moe_intermediate_size)
509
+ for _ in range(cfg.n_shared_experts)
510
+ ]) if cfg.n_shared_experts > 0 else None
511
+
512
+ self._last_aux_loss: Optional[torch.Tensor] = None
513
+
514
+ def forward(self, x: torch.Tensor, position_ids: Optional[torch.Tensor] = None) -> torch.Tensor:
515
+ B, S, H = x.shape
516
+ x_flat = x.view(B * S, H)
517
+ T = B * S
518
+
519
+ # Shared experts: always active (NanoWhale)
520
+ shared_out = torch.zeros_like(x_flat)
521
+ if self.shared_experts:
522
+ for expert in self.shared_experts:
523
+ shared_out = shared_out + expert(x_flat)
524
+ if len(self.shared_experts) > 1:
525
+ shared_out = shared_out / len(self.shared_experts)
526
+
527
+ # Router
528
+ if self.use_hash_routing:
529
+ # Hash routing: deterministic assignment without learned router (NanoWhale).
530
+ # Assign each of the num_experts_per_tok slots a DISTINCT expert by cycling:
531
+ # token at absolute position p -> experts [p%n, (p+1)%n, ..., (p+k-1)%n].
532
+ #
533
+ # BUGFIX: the assignment must key off the token's ABSOLUTE sequence
534
+ # position, not torch.arange(T) (its index in the current flattened
535
+ # batch). With arange(T), incremental KV-cache decoding (S=1) always
536
+ # sees index 0 and routes every token to expert 0, so generation used
537
+ # a different expert assignment than training and silently diverged.
538
+ # Using position_ids makes prefill, full-sequence training, and
539
+ # step-by-step generation all agree. (For S divisible by n_experts,
540
+ # this matches the previous training-time behavior exactly, so existing
541
+ # checkpoints stay valid.)
542
+ if position_ids is not None:
543
+ base = (position_ids.reshape(T, 1) % self.n_routed_experts).long()
544
+ else:
545
+ base = (torch.arange(T, device=x.device) % self.n_routed_experts).unsqueeze(1)
546
+ offsets = torch.arange(self.num_experts_per_tok, device=x.device) # [k]
547
+ top_k_indices = (base + offsets.unsqueeze(0)) % self.n_routed_experts # [T, k]
548
+ top_k_weights = torch.ones(T, self.num_experts_per_tok, device=x.device) / self.num_experts_per_tok
549
+ self._last_aux_loss = None
550
+ else:
551
+ router_logits = self.router(x_flat)
552
+
553
+ if self.scoring_func == "sqrtsoftplus":
554
+ routing_scores = sqrtsoftplus(router_logits)
555
+ else:
556
+ routing_scores = F.softmax(router_logits, dim=-1)
557
+
558
+ top_k_scores, top_k_indices = torch.topk(routing_scores, self.num_experts_per_tok, dim=-1)
559
+
560
+ if self.norm_topk_prob:
561
+ top_k_weights = top_k_scores / (top_k_scores.sum(dim=-1, keepdim=True) + 1e-8)
562
+ else:
563
+ top_k_weights = top_k_scores
564
+ top_k_weights = top_k_weights * self.routed_scaling_factor
565
+
566
+ # Load-balancing aux loss (My Project)
567
+ softmax_probs = F.softmax(router_logits, dim=-1)
568
+ expert_mask = torch.zeros_like(softmax_probs)
569
+ expert_mask.scatter_(1, top_k_indices, 1.0)
570
+ f_e = expert_mask.mean(0)
571
+ p_e = softmax_probs.mean(0)
572
+ self._last_aux_loss = self.n_routed_experts * (f_e * p_e).sum() * self.aux_loss_coef
573
+
574
+ # Dispatch tokens to routed experts
575
+ out = torch.zeros_like(x_flat)
576
+ for expert_idx, expert in enumerate(self.experts):
577
+ token_mask = (top_k_indices == expert_idx).any(dim=-1)
578
+ if not token_mask.any():
579
+ continue
580
+ expert_input = x_flat[token_mask]
581
+ expert_output = expert(expert_input)
582
+ k_pos = (top_k_indices[token_mask] == expert_idx).nonzero(as_tuple=False)
583
+ weights = top_k_weights[token_mask][k_pos[:, 0], k_pos[:, 1]].unsqueeze(-1)
584
+ out[token_mask] = out[token_mask] + expert_output * weights
585
+
586
+ out = out + shared_out
587
+ return out.view(B, S, H)
588
+
589
+ def get_aux_loss(self) -> Optional[torch.Tensor]:
590
+ # Return None when hash routing (no aux loss) or when forward hasn't run yet.
591
+ # Returning torch.tensor(0.0) here would be a CPU tensor and cause a device
592
+ # mismatch when added to the CUDA total_aux_loss in SpikeWhaleModel.
593
+ return self._last_aux_loss
594
+
595
+
596
+ class DenseFFN(nn.Module):
597
+ """Dense SwiGLU FFN for non-MoE layers."""
598
+ def __init__(self, cfg: SpikeWhaleConfig):
599
+ super().__init__()
600
+ self.gate_proj = nn.Linear(cfg.hidden_size, cfg.moe_intermediate_size, bias=False)
601
+ self.up_proj = nn.Linear(cfg.hidden_size, cfg.moe_intermediate_size, bias=False)
602
+ self.down_proj = nn.Linear(cfg.moe_intermediate_size, cfg.hidden_size, bias=False)
603
+
604
+ def forward(self, x: torch.Tensor, position_ids: Optional[torch.Tensor] = None) -> torch.Tensor:
605
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
606
+
607
+ def get_aux_loss(self) -> Optional[torch.Tensor]:
608
+ return None # dense layers have no aux loss; None avoids CPU-tensor device mismatch
609
+
610
+
611
+ # ---------------------------------------------------------------------------
612
+ # Transformer block with Hyper-Connections
613
+ # ---------------------------------------------------------------------------
614
+
615
+ class TransformerBlock(nn.Module):
616
+ """
617
+ Transformer block combining all features:
618
+ - Hyper-Connections: pre/post routing through hc_mult streams (NanoWhale)
619
+ - MLA + DERF + XSA attention (combined)
620
+ - MoE FFN with shared expert (NanoWhale) + aux loss (My Project)
621
+ """
622
+ def __init__(self, cfg: SpikeWhaleConfig, layer_idx: int):
623
+ super().__init__()
624
+ self.use_hc = cfg.use_hyper_connections
625
+ self.hidden_dropout = cfg.hidden_dropout
626
+
627
+ self.attn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
628
+ self.attn = MLADerfXSAAttention(cfg)
629
+ self.ffn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
630
+
631
+ if cfg.use_moe and layer_idx in cfg.moe_layers:
632
+ self.ffn = SparseMoEFFN(cfg, layer_idx)
633
+ self.is_moe = True
634
+ else:
635
+ self.ffn = DenseFFN(cfg)
636
+ self.is_moe = False
637
+
638
+ if self.use_hc:
639
+ self.hc_attn = HyperConnectionLayer(cfg.hidden_size, cfg.hc_mult,
640
+ cfg.hc_sinkhorn_iters, cfg.hc_eps)
641
+ self.hc_ffn = HyperConnectionLayer(cfg.hidden_size, cfg.hc_mult,
642
+ cfg.hc_sinkhorn_iters, cfg.hc_eps)
643
+
644
+ def forward(
645
+ self,
646
+ x: torch.Tensor, # [B, hc_mult, S, H] if HC else [B, S, H]
647
+ position_ids: torch.Tensor,
648
+ attention_mask: Optional[torch.Tensor] = None,
649
+ past_key_value: Optional[Tuple] = None,
650
+ use_cache: bool = False,
651
+ ) -> Tuple[torch.Tensor, Optional[Tuple], Optional[torch.Tensor]]:
652
+
653
+ # --- Attention sub-layer ---
654
+ if self.use_hc:
655
+ h = self.hc_attn.pre_op(x) # [B, S, H]
656
+ else:
657
+ h = x
658
+
659
+ attn_out, present = self.attn(
660
+ self.attn_norm(h), position_ids, attention_mask, past_key_value, use_cache
661
+ )
662
+ attn_out = F.dropout(attn_out, p=self.hidden_dropout, training=self.training)
663
+
664
+ if self.use_hc:
665
+ x = self.hc_attn.post_op(x, attn_out)
666
+ h = self.hc_ffn.pre_op(x) # [B, S, H]
667
+ else:
668
+ h = h + attn_out
669
+
670
+ # --- FFN sub-layer ---
671
+ ffn_out = self.ffn(self.ffn_norm(h), position_ids)
672
+ ffn_out = F.dropout(ffn_out, p=self.hidden_dropout, training=self.training)
673
+
674
+ if self.use_hc:
675
+ x = self.hc_ffn.post_op(x, ffn_out)
676
+ else:
677
+ x = h + ffn_out
678
+
679
+ return x, present, self.ffn.get_aux_loss()
680
+
681
+
682
+ # ---------------------------------------------------------------------------
683
+ # Full model
684
+ # ---------------------------------------------------------------------------
685
+
686
+ class HRMRefinementBlock(nn.Module):
687
+ """
688
+ HRM-INSPIRED iterative refinement (EXPERIMENTAL, off by default). NOT the full
689
+ Hierarchical Reasoning Model -- only the iterative-refinement mechanism that the
690
+ independent ARC-Prize ablation found carried most of HRM's benefit, adapted to a
691
+ causal LM's final hidden state.
692
+
693
+ Runs N inner steps; each computes a small gated update conditioned on the current
694
+ state AND the original ('anchor') input. Per-step gate inits at 0 and up.weight is
695
+ zero-init -> the block is an EXACT identity at init, so enabling it cannot hurt a
696
+ fresh model; it only contributes if training opens the gate. Pointwise over
697
+ positions -> causal-safe (no future-token leakage). In/out [B,S,H].
698
+ """
699
+ def __init__(self, hidden_size: int, refine_dim: int, steps: int, eps: float = 1e-6):
700
+ super().__init__()
701
+ self.steps = steps
702
+ self.norm = RMSNorm(hidden_size, eps)
703
+ self.down = nn.Linear(hidden_size * 2, refine_dim, bias=False)
704
+ self.up = nn.Linear(refine_dim, hidden_size, bias=False)
705
+ self.gate = nn.Parameter(torch.zeros(steps))
706
+ nn.init.normal_(self.down.weight, std=0.02)
707
+ nn.init.zeros_(self.up.weight)
708
+
709
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
710
+ anchor = x
711
+ h = x
712
+ for t in range(self.steps):
713
+ inp = torch.cat([self.norm(h), anchor], dim=-1)
714
+ update = self.up(F.silu(self.down(inp)))
715
+ h = h + torch.tanh(self.gate[t]) * update
716
+ return h
717
+
718
+
719
+ class LatentProjection(nn.Module):
720
+ """ModularMind-on-V2: pool final hidden state -> d_latent output vector.
721
+ Mirrors ModularMind's contract: mean-pool over sequence, ReLU^2 activation
722
+ (sparse latent codes), Xavier init (NOT zero) so the latent carries signal
723
+ from step 1 — zero-init would make the chain unable to bootstrap."""
724
+ def __init__(self, hidden_size: int, d_latent: int, eps: float = 1e-6):
725
+ super().__init__()
726
+ self.proj1 = nn.Linear(hidden_size, hidden_size, bias=False)
727
+ self.proj2 = nn.Linear(hidden_size, d_latent, bias=False)
728
+ self.norm = RMSNorm(d_latent, eps)
729
+ nn.init.xavier_uniform_(self.proj1.weight)
730
+ nn.init.xavier_uniform_(self.proj2.weight)
731
+
732
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
733
+ pooled = x.mean(dim=1) # [B, S, H] -> [B, H]
734
+ h = torch.relu(self.proj1(pooled)) ** 2
735
+ return self.norm(self.proj2(h)) # [B, d_latent]
736
+
737
+
738
+ class LatentInjection(nn.Module):
739
+ """ModularMind-on-V2: fold an incoming d_latent vector into embeddings.
740
+ Broadcast across positions, ReGLU-gated add. Gate starts SMALL (not exactly
741
+ zero): the injection is near-identity at init (stable) while still passing a
742
+ little gradient, so the upstream RecursiveLink + specialist can bootstrap from
743
+ step 1. (Exact-zero gate would block all gradient to the link -- the
744
+ bootstrapping problem ModularMind's LatentProjection docstring warns about.)
745
+ This is the INPUT side of RecursiveLink (the prev specialist's latent)."""
746
+ def __init__(self, hidden_size: int, d_latent: int, eps: float = 1e-6,
747
+ gate_init: float = 1e-3):
748
+ super().__init__()
749
+ self.up = nn.Linear(d_latent, hidden_size, bias=False)
750
+ self.norm = RMSNorm(hidden_size, eps)
751
+ self.value_proj = nn.Linear(hidden_size, hidden_size, bias=False)
752
+ self.gate_proj = nn.Linear(hidden_size, hidden_size, bias=False)
753
+ self.gate_init = gate_init
754
+ nn.init.xavier_uniform_(self.up.weight)
755
+ nn.init.xavier_uniform_(self.value_proj.weight)
756
+ nn.init.normal_(self.gate_proj.weight, std=gate_init) # small, not zero
757
+
758
+ def forward(self, x: torch.Tensor, latent: torch.Tensor) -> torch.Tensor:
759
+ # x: [B, S, H], latent: [B, d_latent]
760
+ inj = self.norm(self.up(latent)).unsqueeze(1) # [B, 1, H] broadcast over S
761
+ value = self.value_proj(inj)
762
+ gate = torch.relu(self.gate_proj(inj))
763
+ return x + value * gate
764
+
765
+
766
+ class RecursiveLink(nn.Module):
767
+ """ModularMind cross-specialist bridge, V2 build. Converts one specialist's
768
+ output latent into the next specialist's input latent. ReGLU + residual,
769
+ single shared module reused for every hop. Fully differentiable."""
770
+ def __init__(self, d_latent: int = 256, expansion: float = 2.0):
771
+ super().__init__()
772
+ d_hidden = int(d_latent * expansion)
773
+ self.norm = nn.LayerNorm(d_latent)
774
+ self.value_proj = nn.Linear(d_latent, d_hidden, bias=False)
775
+ self.gate_proj = nn.Linear(d_latent, d_hidden, bias=False)
776
+ self.down = nn.Linear(d_hidden, d_latent, bias=False)
777
+ self.residual_gate = nn.Parameter(torch.ones(1))
778
+ nn.init.xavier_uniform_(self.value_proj.weight)
779
+ nn.init.xavier_uniform_(self.gate_proj.weight)
780
+ nn.init.xavier_uniform_(self.down.weight)
781
+
782
+ def forward(self, z: torch.Tensor) -> torch.Tensor:
783
+ n = self.norm(z)
784
+ h = self.value_proj(n) * torch.relu(self.gate_proj(n))
785
+ return z + self.residual_gate * self.down(h)
786
+
787
+
788
+ class SpikeWhaleModel(nn.Module):
789
+ """Decoder stack without LM head."""
790
+
791
+ def __init__(self, cfg: SpikeWhaleConfig):
792
+ super().__init__()
793
+ self.cfg = cfg
794
+ self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
795
+ nn.init.normal_(self.embed_tokens.weight, std=cfg.initializer_range)
796
+
797
+ self.engram = EngramModule(cfg) if cfg.use_engram else None
798
+ self.layers = nn.ModuleList([
799
+ TransformerBlock(cfg, layer_idx=i)
800
+ for i in range(cfg.num_hidden_layers)
801
+ ])
802
+ self.norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
803
+ self.hrm_refine = (
804
+ HRMRefinementBlock(cfg.hidden_size, cfg.hrm_refine_dim,
805
+ cfg.hrm_refine_steps, cfg.rms_norm_eps)
806
+ if getattr(cfg, "use_hrm_refine", False) else None
807
+ )
808
+ # ModularMind-on-V2: latent input/output (off unless use_latent_io)
809
+ if getattr(cfg, "use_latent_io", False):
810
+ self.latent_inject = LatentInjection(cfg.hidden_size, cfg.d_latent, cfg.rms_norm_eps)
811
+ self.latent_out = LatentProjection(cfg.hidden_size, cfg.d_latent, cfg.rms_norm_eps)
812
+ else:
813
+ self.latent_inject = None
814
+ self.latent_out = None
815
+ self.gradient_checkpointing = False
816
+
817
+ def reset_latent_gate(self):
818
+ """Re-init the injection gate SMALL (not zero). Must be called AFTER any HF
819
+ post_init/_init_weights pass, which otherwise re-randomizes the gate to full
820
+ scale. Small-but-nonzero keeps injection near-identity at start while letting
821
+ gradient reach the upstream RecursiveLink (so the chain can bootstrap)."""
822
+ if self.latent_inject is not None:
823
+ nn.init.normal_(self.latent_inject.gate_proj.weight,
824
+ std=self.latent_inject.gate_init)
825
+
826
+ def forward(
827
+ self,
828
+ input_ids: torch.Tensor,
829
+ attention_mask: Optional[torch.Tensor] = None,
830
+ position_ids: Optional[torch.Tensor] = None,
831
+ past_key_values: Optional[List[Tuple]] = None,
832
+ use_cache: bool = False,
833
+ inject_latent: Optional[torch.Tensor] = None,
834
+ ) -> Tuple[torch.Tensor, Optional[List[Tuple]], torch.Tensor]:
835
+ B, S = input_ids.shape
836
+ device = input_ids.device
837
+
838
+ if position_ids is None:
839
+ past_len = past_key_values[0][0].shape[2] if past_key_values else 0
840
+ position_ids = torch.arange(
841
+ past_len, past_len + S, device=device
842
+ ).unsqueeze(0).expand(B, -1)
843
+
844
+ # Token embedding
845
+ x = self.embed_tokens(input_ids) # [B, S, H]
846
+
847
+ # Engram N-gram delta (My Project)
848
+ if self.engram is not None:
849
+ x = x + self.engram(x)
850
+
851
+ # ModularMind-on-V2: inject the previous specialist's latent (broadcast
852
+ # across positions, ReGLU-gated). No-op at init (gate zero) and skipped
853
+ # entirely if no latent is passed.
854
+ if self.latent_inject is not None and inject_latent is not None:
855
+ x = self.latent_inject(x, inject_latent)
856
+
857
+ # Expand to hc_mult streams for Hyper-Connections (NanoWhale)
858
+ if self.cfg.use_hyper_connections:
859
+ x = x.unsqueeze(1).expand(-1, self.cfg.hc_mult, -1, -1).clone()
860
+ # [B, hc_mult, S, H]
861
+
862
+ present_key_values = [] if use_cache else None
863
+ total_aux_loss = torch.tensor(0.0, device=device)
864
+
865
+ for layer_idx, layer in enumerate(self.layers):
866
+ pkv = past_key_values[layer_idx] if past_key_values else None
867
+
868
+ if self.gradient_checkpointing and self.training:
869
+ # Gradient checkpointing with use_reentrant=False (NanoWhale)
870
+ x, present, aux_loss = gradient_checkpoint(
871
+ layer, x, position_ids, attention_mask, None, False,
872
+ use_reentrant=False,
873
+ )
874
+ else:
875
+ x, present, aux_loss = layer(x, position_ids, attention_mask, pkv, use_cache)
876
+
877
+ if use_cache:
878
+ present_key_values.append(present)
879
+ if aux_loss is not None:
880
+ total_aux_loss = total_aux_loss + aux_loss
881
+
882
+ # Reduce HC streams to single hidden state
883
+ if self.cfg.use_hyper_connections:
884
+ x = x.mean(dim=1) # [B, S, H]
885
+
886
+ if self.hrm_refine is not None:
887
+ x = self.hrm_refine(x)
888
+
889
+ x = self.norm(x)
890
+
891
+ # ModularMind-on-V2: emit this specialist's output latent (for RecursiveLink).
892
+ out_latent = self.latent_out(x) if self.latent_out is not None else None
893
+ return x, present_key_values, total_aux_loss, out_latent
894
+
895
+
896
+ class SpikeWhaleLM(PreTrainedModel):
897
+ """
898
+ Full causal LM combining all SpikeTransformer + NanoWhale features.
899
+
900
+ Training (forward with labels):
901
+ out = model(input_ids=ids, labels=ids)
902
+ loss = out.loss # CE + MTP loss + MoE aux loss
903
+
904
+ Generation:
905
+ out = model(input_ids=ids, use_cache=True)
906
+ past = out.past_key_values
907
+ out2 = model(input_ids=next_id, past_key_values=past, use_cache=True)
908
+ """
909
+ config_class = SpikeWhaleConfig
910
+ base_model_prefix = "model"
911
+ supports_gradient_checkpointing = True
912
+ _no_split_modules = ["TransformerBlock"]
913
+
914
+ def __init__(self, cfg: SpikeWhaleConfig):
915
+ super().__init__(cfg)
916
+ self.model = SpikeWhaleModel(cfg)
917
+ self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
918
+ nn.init.normal_(self.lm_head.weight, std=cfg.initializer_range)
919
+
920
+ if cfg.tie_word_embeddings:
921
+ self.lm_head.weight = self.model.embed_tokens.weight
922
+
923
+ # Multi-Token Prediction heads (NanoWhale): predict token at position+k
924
+ self.mtp_heads = nn.ModuleList([
925
+ nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
926
+ for _ in range(cfg.num_nextn_predict_layers)
927
+ ]) if cfg.num_nextn_predict_layers > 0 else None
928
+
929
+ self.post_init()
930
+ # HF post_init re-randomizes Linear weights, clobbering the zero-init
931
+ # injection gate. Restore it so the latent injection is identity-at-start.
932
+ self.model.reset_latent_gate()
933
+
934
+ def get_input_embeddings(self):
935
+ return self.model.embed_tokens
936
+
937
+ def set_input_embeddings(self, value):
938
+ self.model.embed_tokens = value
939
+
940
+ def get_output_embeddings(self):
941
+ return self.lm_head
942
+
943
+ def set_output_embeddings(self, new_embeddings):
944
+ self.lm_head = new_embeddings
945
+
946
+ def _set_gradient_checkpointing(self, module, value=False):
947
+ if isinstance(module, SpikeWhaleModel):
948
+ module.gradient_checkpointing = value
949
+
950
+ def forward(
951
+ self,
952
+ input_ids: Optional[torch.Tensor] = None,
953
+ attention_mask: Optional[torch.Tensor] = None,
954
+ position_ids: Optional[torch.Tensor] = None,
955
+ past_key_values: Optional[List[Tuple]] = None,
956
+ labels: Optional[torch.Tensor] = None,
957
+ use_cache: bool = False,
958
+ inject_latent: Optional[torch.Tensor] = None,
959
+ **kwargs,
960
+ ) -> CausalLMOutputWithPast:
961
+ hidden, present_kvs, aux_loss, out_latent = self.model(
962
+ input_ids=input_ids,
963
+ attention_mask=attention_mask,
964
+ position_ids=position_ids,
965
+ past_key_values=past_key_values,
966
+ use_cache=use_cache,
967
+ inject_latent=inject_latent,
968
+ )
969
+
970
+ logits = self.lm_head(hidden)
971
+ loss = None
972
+
973
+ if labels is not None:
974
+ # Standard next-token CE loss (shifted by 1)
975
+ shift_logits = logits[..., :-1, :].contiguous()
976
+ shift_labels = labels[..., 1:].contiguous()
977
+ loss = F.cross_entropy(
978
+ shift_logits.view(-1, shift_logits.size(-1)),
979
+ shift_labels.view(-1),
980
+ ignore_index=-100,
981
+ )
982
+
983
+ # Multi-Token Prediction loss (NanoWhale)
984
+ # Each MTP head k predicts token at position + k+1 (beyond the standard +1)
985
+ if self.mtp_heads is not None:
986
+ mtp_total = torch.tensor(0.0, device=loss.device)
987
+ for k, head in enumerate(self.mtp_heads, start=1):
988
+ offset = k + 1 # predicts position + offset
989
+ if hidden.size(1) > offset:
990
+ mtp_logits = head(hidden[..., :-offset, :].contiguous())
991
+ mtp_labels = labels[..., offset:].contiguous()
992
+ mtp_total = mtp_total + F.cross_entropy(
993
+ mtp_logits.view(-1, mtp_logits.size(-1)),
994
+ mtp_labels.view(-1),
995
+ ignore_index=-100,
996
+ )
997
+ loss = loss + mtp_total / max(len(self.mtp_heads), 1)
998
+
999
+ # MoE load-balancing aux loss (My Project)
1000
+ loss = loss + aux_loss
1001
+
1002
+ out = CausalLMOutputWithPast(
1003
+ loss=loss,
1004
+ logits=logits,
1005
+ past_key_values=present_kvs,
1006
+ )
1007
+ out.latent = out_latent # ModularMind-on-V2: this specialist's output latent
1008
+ return out
1009
+
1010
+ def count_parameters(self) -> int:
1011
+ return sum(p.numel() for p in self.parameters())
agents/modmind/moe_gradio.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ moe_gradio.py -- adapter that drives the agents/ Gradio MoE panel with the ModMind
3
+ SpikeWhale specialists instead of the byte-level ByteGPT experts.
4
+
5
+ It exposes the SAME public API as agents/orchestrator.py:
6
+ get_moe(device) -> obj with .available() .reload() .route() .shared_latent()
7
+ .generate() .run()
8
+ and .run() returns the SAME dict shape panel.py consumes:
9
+ {winner, weights, bits_per_byte, steps, generation, shared_latent}
10
+ so agents/panel.py can use it as a drop-in backend (see the MM_MOE_BACKEND switch there).
11
+
12
+ What this adapter handles (the real differences vs the byte-level experts):
13
+ * Each specialist has its OWN SpikeTokenizer (different vocab), so per-TOKEN NLL is
14
+ not comparable across experts. Routing is therefore by BITS-PER-BYTE -- total NLL
15
+ divided by the query's raw UTF-8 byte count -- which IS comparable across tokenizers.
16
+ * d_latent=256 (vs 64); the shared RecursiveLink is built at 256, and every ModMind
17
+ specialist already shares that width, so latent fusion works natively.
18
+ * Checkpoints are ModMind format ({"model_state": ...}) under
19
+ <MM_CKPT_ROOT or repo>/<domain>/checkpoints/step_*.pt, loaded exactly like
20
+ train_link.py does, and hot-reloaded when newer ones appear.
21
+
22
+ Display mapping (ModMind domain -> the panel's expert slot/emoji):
23
+ language -> language (the panel shows 📖), reasoning -> math (➗), tool_use -> tool (🛠️)
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import glob
28
+ import math
29
+ import os
30
+ import random
31
+ import string
32
+ from pathlib import Path
33
+
34
+ import torch
35
+ import torch.nn.functional as F
36
+
37
+ from model import RecursiveLink, SpikeWhaleLM
38
+ from specialist_presets import specialist_config
39
+ from spike_tokenizer import SpikeTokenizer
40
+
41
+ HERE = Path(__file__).parent
42
+ # checkpoints may live on a Modal Volume; tokenizers stay with the code (same rule as train_link.py)
43
+ CKPT_ROOT = Path(os.environ.get("MM_CKPT_ROOT", str(HERE)))
44
+ D_LATENT = 256
45
+ MAX_CTX = 512 # cap prompt/scoring length so CPU routing + generation stay snappy
46
+
47
+ # (modmind_domain, panel_slot). reasoning trains on FineMath, so it IS the "math" expert.
48
+ SLOTS = [("language", "language"), ("reasoning", "math"), ("tool_use", "tool")]
49
+ DOMAIN2SLOT = {d: s for d, s in SLOTS}
50
+ # trained bridges to surface in the panel as (asker_domain, consultant_domain) "consult" demos
51
+ LINKS = [("language", "reasoning")]
52
+ KEY_CHARS = string.ascii_letters + string.digits # must match train_link.py
53
+
54
+
55
+ def _latest_ckpt(domain: str):
56
+ cks = sorted(glob.glob(str(CKPT_ROOT / domain / "checkpoints" / "step_*.pt")))
57
+ return cks[-1] if cks else None
58
+
59
+
60
+ class SpikeWhaleMoE:
61
+ def __init__(self, device: str = "cpu"):
62
+ self.device = device
63
+ self.models, self.toks, self.steps, self._mtime = {}, {}, {}, {}
64
+ self.link = RecursiveLink(d_latent=D_LATENT).to(device).eval()
65
+ self.trained_link = None # the TRAINED bridge (train_link.py output), for the consult demo
66
+ self.bridge_asker = None # the FULL fine-tuned asker, for reproducible key-recall
67
+ self.link_meta = None
68
+ self.reload()
69
+
70
+ def reload(self):
71
+ """Load/refresh any specialist whose checkpoint exists or changed on disk."""
72
+ for domain, slot in SLOTS:
73
+ ck = _latest_ckpt(domain)
74
+ tok_path = HERE / domain / "tokenizer.json"
75
+ if ck is None or not tok_path.exists():
76
+ continue
77
+ mt = os.path.getmtime(ck)
78
+ if self._mtime.get(slot) == mt:
79
+ continue
80
+ cfg = specialist_config(domain)
81
+ m = SpikeWhaleLM(cfg).to(self.device).eval()
82
+ st = torch.load(ck, map_location=self.device, weights_only=False)
83
+ m.load_state_dict(st["model_state"])
84
+ self.models[slot] = m
85
+ self.toks[slot] = SpikeTokenizer(vocab_file=str(tok_path))
86
+ self.steps[slot] = int(st.get("step", 0))
87
+ self._mtime[slot] = mt
88
+ self._load_links()
89
+ return list(self.models)
90
+
91
+ def available(self):
92
+ return list(self.models)
93
+
94
+ def _load_links(self):
95
+ """Load a TRAINED RecursiveLink bridge (train_link.py output) from
96
+ links/<asker>__from__<consultant>.pt and overlay its bridge-trained injection path
97
+ onto the asker. The injection only fires when we pass inject_latent (the consult demo),
98
+ so normal routing/generation is unaffected."""
99
+ self.trained_link = None
100
+ self.bridge_asker = None
101
+ self.link_meta = None
102
+ for asker_domain, consultant_domain in LINKS:
103
+ a, c = DOMAIN2SLOT.get(asker_domain), DOMAIN2SLOT.get(consultant_domain)
104
+ if a not in self.models or c not in self.models:
105
+ continue
106
+ lp = CKPT_ROOT / "links" / f"{asker_domain}__from__{consultant_domain}.pt"
107
+ if not lp.exists():
108
+ continue
109
+ st = torch.load(lp, map_location=self.device, weights_only=False)
110
+ link = RecursiveLink(d_latent=D_LATENT).to(self.device).eval()
111
+ link.load_state_dict(st["link"])
112
+ try:
113
+ self.models[a].model.latent_inject.load_state_dict(st["asker_latent_inject"])
114
+ except Exception:
115
+ pass
116
+ # FULL fine-tuned asker, if this bridge was saved with the updated train_link.py.
117
+ # Without it we can show latent *influence* (consult) but not reproduce key-recall.
118
+ if "asker_state" in st:
119
+ ba = SpikeWhaleLM(specialist_config(asker_domain)).to(self.device).eval()
120
+ ba.load_state_dict(st["asker_state"])
121
+ self.bridge_asker = ba
122
+ self.trained_link = link
123
+ self.link_meta = {
124
+ "asker": a, "consultant": c,
125
+ "key_len": int(st.get("key_len", 6)), "prompt": st.get("prompt", "KEY> "),
126
+ "with_latent": float(st.get("with_latent", float("nan"))),
127
+ "without_latent": float(st.get("without_latent", float("nan"))),
128
+ }
129
+ return # one bridge is enough for the panel demo
130
+
131
+ def key_recall_available(self):
132
+ return self.bridge_asker is not None and self.trained_link is not None
133
+
134
+ @torch.no_grad()
135
+ def key_recall(self, n: int = 8, ablate: bool = False):
136
+ """Reproduce the train_link.py forcing task with the FULL trained asker: a random key
137
+ is shown ONLY to the consultant; the asker must emit it from the latent alone.
138
+ Returns {acc, examples:[(key, recovered, ok)]}. ablate=True zeros the latent."""
139
+ if not self.key_recall_available():
140
+ return None
141
+ a, c = self.link_meta["asker"], self.link_meta["consultant"]
142
+ a_tok, c_tok = self.toks[a], self.toks[c]
143
+ key_len = self.link_meta.get("key_len", 6)
144
+ prompt = self.link_meta.get("prompt", "KEY> ")
145
+ plen = len(a_tok.encode(prompt, add_special_tokens=False))
146
+ keys = ["".join(random.choice(KEY_CHARS) for _ in range(key_len)) for _ in range(n)]
147
+ examples, correct = [], 0
148
+ for k in keys:
149
+ c_ids = torch.tensor([c_tok.encode(k, add_special_tokens=False)], device=self.device)
150
+ a_ids = torch.tensor([a_tok.encode(prompt, add_special_tokens=False)
151
+ + a_tok.encode(k, add_special_tokens=False)], device=self.device)
152
+ latent = self.models[c](input_ids=c_ids).latent
153
+ inj = torch.zeros_like(self.trained_link(latent)) if ablate else self.trained_link(latent)
154
+ logits = self.bridge_asker(input_ids=a_ids, inject_latent=inj).logits
155
+ pred = logits[:, plen - 1:plen - 1 + key_len, :].argmax(-1)[0]
156
+ out = a_tok.decode(pred.tolist())[:len(k)]
157
+ ok = (out == k)
158
+ correct += int(ok)
159
+ examples.append((k, out, ok))
160
+ return {"acc": correct / max(1, n), "examples": examples}
161
+
162
+ def consult_available(self):
163
+ return self.trained_link is not None
164
+
165
+ def consult_meta(self):
166
+ return dict(self.link_meta) if self.link_meta else None
167
+
168
+ @torch.no_grad()
169
+ def consult(self, query: str, max_new: int = 120, temperature: float = 0.8,
170
+ top_k: int = 40, ablate: bool = False):
171
+ """The asker generates while reading the consultant's latent through the TRAINED
172
+ RecursiveLink. ablate=True zeros the latent (the truth-test: output should lose the
173
+ consultant's influence)."""
174
+ if not self.consult_available():
175
+ return ""
176
+ a, c = self.link_meta["asker"], self.link_meta["consultant"]
177
+ latent = self.models[c](input_ids=self._ids(c, query)).latent # [1, 256]
178
+ inj = self.trained_link(latent)
179
+ if ablate:
180
+ inj = torch.zeros_like(inj)
181
+ m, tok = self.models[a], self.toks[a]
182
+ ids = self._ids(a, query); start = ids.shape[1]
183
+ ctx_max = int(getattr(m.config, "max_position_embeddings", 2048))
184
+ for _ in range(max_new):
185
+ logits = m(input_ids=ids[:, -ctx_max:], inject_latent=inj).logits[:, -1, :] / max(1e-5, temperature)
186
+ if top_k:
187
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
188
+ logits[logits < v[:, [-1]]] = -float("inf")
189
+ nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
190
+ ids = torch.cat([ids, nxt], dim=1)
191
+ if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
192
+ break
193
+ return tok.decode(ids[0, start:].tolist())
194
+
195
+ def combine_available(self):
196
+ return "language" in self.models and "math" in self.models
197
+
198
+ @torch.no_grad()
199
+ def combine(self, query: str, max_new: int = 60, blend: float = 0.5,
200
+ temperature: float = 0.8, top_k: int = 40, consult: bool = False):
201
+ """Token-level MoE: blend BOTH specialists' next-token distributions every step.
202
+ Possible because they share the 16k tokenizer (same vocab). blend = weight on Math
203
+ (0 -> pure Language, 1 -> pure Math, 0.5 -> equal mix). consult=True also injects Math's
204
+ latent into Language through the trained RecursiveLink during the blend."""
205
+ if not self.combine_available():
206
+ return ""
207
+ lang, math_ = self.models["language"], self.models["math"]
208
+ tok = self.toks["language"] # shared tokenizer (identical for both)
209
+ ids = self._ids("language", query); start = ids.shape[1]
210
+ cl = int(getattr(lang.config, "max_position_embeddings", 2048))
211
+ cm = int(getattr(math_.config, "max_position_embeddings", 2048))
212
+ inj = None
213
+ if consult and self.trained_link is not None:
214
+ latent = math_(input_ids=self._ids("math", query)).latent
215
+ inj = self.trained_link(latent)
216
+ t = max(1e-5, temperature)
217
+ for _ in range(max_new):
218
+ pl = F.softmax(lang(input_ids=ids[:, -cl:], inject_latent=inj).logits[:, -1, :] / t, dim=-1)
219
+ pm = F.softmax(math_(input_ids=ids[:, -cm:]).logits[:, -1, :] / t, dim=-1)
220
+ probs = (1.0 - blend) * pl + blend * pm # mixture of the two experts' distributions
221
+ if top_k:
222
+ v, _ = torch.topk(probs, min(top_k, probs.size(-1)))
223
+ probs = torch.where(probs < v[:, [-1]], torch.zeros_like(probs), probs)
224
+ probs = probs / probs.sum(dim=-1, keepdim=True)
225
+ nxt = torch.multinomial(probs, 1)
226
+ ids = torch.cat([ids, nxt], dim=1)
227
+ if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
228
+ break
229
+ return tok.decode(ids[0, start:].tolist())
230
+
231
+ def _ids(self, slot: str, text: str):
232
+ tok = self.toks[slot]
233
+ ids = tok.encode(text, add_special_tokens=False)[:MAX_CTX] or [tok.pad_token_id or 0]
234
+ return torch.tensor([ids], dtype=torch.long, device=self.device)
235
+
236
+ @torch.no_grad()
237
+ def _bits_per_byte(self, slot: str, text: str) -> float:
238
+ """Total next-token NLL of `text` under this specialist, per RAW UTF-8 byte.
239
+ Byte-normalisation makes the score comparable across the experts' different
240
+ tokenizers (a smaller vocab spends more tokens but each is cheaper)."""
241
+ nbytes = max(1, len(text.encode("utf-8")))
242
+ ids = self._ids(slot, text)
243
+ if ids.shape[1] < 2:
244
+ return float("inf")
245
+ logits = self.models[slot](input_ids=ids).logits
246
+ ce = F.cross_entropy(logits[:, :-1, :].reshape(-1, logits.size(-1)),
247
+ ids[:, 1:].reshape(-1), reduction="sum")
248
+ return (ce.item() / math.log(2)) / nbytes
249
+
250
+ @torch.no_grad()
251
+ def route(self, query: str, temp: float = 0.5):
252
+ """Per-expert bits/byte + softmax routing weights + the winner (lowest bits/byte)."""
253
+ bits = {slot: self._bits_per_byte(slot, query) for slot in self.models}
254
+ names = list(bits)
255
+ logits = torch.tensor([-bits[n] / temp for n in names])
256
+ w = F.softmax(logits, dim=0).tolist()
257
+ weights = {n: round(wi, 3) for n, wi in zip(names, w)}
258
+ winner = min(bits, key=bits.get)
259
+ return winner, weights, {n: round(b, 3) for n, b in bits.items()}
260
+
261
+ @torch.no_grad()
262
+ def shared_latent(self, query: str):
263
+ """Each expert's output latent -> sum -> RecursiveLink -> one shared latent (the bus)."""
264
+ lats = {slot: self.models[slot](input_ids=self._ids(slot, query)).latent
265
+ for slot in self.models}
266
+ z = torch.stack([lats[s] for s in lats], 0).sum(0) # [1, 256]
267
+ shared = self.link(z)[0]
268
+ return {s: lats[s][0].tolist() for s in lats}, shared.tolist()
269
+
270
+ @torch.no_grad()
271
+ def generate(self, query: str, expert: str | None = None, max_new: int = 160,
272
+ temperature: float = 0.8, top_k: int = 40):
273
+ if expert is None:
274
+ expert, _, _ = self.route(query)
275
+ m, tok = self.models[expert], self.toks[expert]
276
+ ids = self._ids(expert, query)
277
+ start = ids.shape[1]
278
+ ctx_max = int(getattr(m.config, "max_position_embeddings", 2048))
279
+ for _ in range(max_new):
280
+ logits = m(input_ids=ids[:, -ctx_max:]).logits[:, -1, :] / max(1e-5, temperature)
281
+ if top_k:
282
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
283
+ logits[logits < v[:, [-1]]] = -float("inf")
284
+ nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
285
+ ids = torch.cat([ids, nxt], dim=1)
286
+ if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
287
+ break
288
+ return expert, tok.decode(ids[0, start:].tolist())
289
+
290
+ @torch.no_grad()
291
+ def run(self, query: str, max_new: int = 160, temperature: float = 0.8):
292
+ """Full pass: route -> fuse latents -> generate from the winner."""
293
+ if not self.models:
294
+ return {"error": "no experts trained yet"}
295
+ winner, weights, bits = self.route(query)
296
+ _, shared = self.shared_latent(query)
297
+ _, gen = self.generate(query, winner, max_new, temperature)
298
+ return {
299
+ "winner": winner, "weights": weights, "bits_per_byte": bits,
300
+ "steps": {n: self.steps.get(n, 0) for n in self.models},
301
+ "generation": gen, "shared_latent": [round(x, 3) for x in shared],
302
+ }
303
+
304
+ # ------------------------------------------------------------------ #
305
+ # WEIGHT-MERGE section (self-contained; does not touch anything above).
306
+ # Both specialists are the identical dense architecture, so their weights
307
+ # can be linearly merged into ONE model: W = (1-alpha)*Language + alpha*Math.
308
+ # This is a real merged model (one forward pass), not an output ensemble.
309
+ # ------------------------------------------------------------------ #
310
+ def merge_available(self):
311
+ return "language" in self.models and "math" in self.models
312
+
313
+ def _merged_model(self, alpha: float):
314
+ """Build (and cache the most recent) single weight-merged model at ratio alpha."""
315
+ alpha = round(float(alpha), 2)
316
+ cache = getattr(self, "_merge_cache", None)
317
+ if cache is not None and abs(cache[0] - alpha) < 1e-6:
318
+ return cache[1]
319
+ lsd = self.models["language"].state_dict()
320
+ msd = self.models["math"].state_dict()
321
+ merged = {}
322
+ for k in lsd:
323
+ if lsd[k].is_floating_point():
324
+ merged[k] = ((1.0 - alpha) * lsd[k].float() + alpha * msd[k].float()).to(lsd[k].dtype)
325
+ else:
326
+ merged[k] = lsd[k].clone() # integer/bool buffers: copy as-is
327
+ m = SpikeWhaleLM(specialist_config("language")).to(self.device).eval()
328
+ m.load_state_dict(merged)
329
+ # make it bridge-ready: re-load the bridge-trained injection weights straight from the
330
+ # link file (self-contained). The injection only fires when we pass inject_latent.
331
+ try:
332
+ lp = CKPT_ROOT / "links" / "language__from__reasoning.pt"
333
+ if lp.exists():
334
+ st = torch.load(lp, map_location=self.device, weights_only=False)
335
+ m.model.latent_inject.load_state_dict(st["asker_latent_inject"])
336
+ except Exception:
337
+ pass
338
+ self._merge_cache = (alpha, m)
339
+ return m
340
+
341
+ @torch.no_grad()
342
+ def merge_generate(self, query: str, alpha: float = 0.5, max_new: int = 60,
343
+ temperature: float = 0.8, top_k: int = 40, consult: bool = False):
344
+ """Generate from the WEIGHT-MERGED single model. consult=True injects Math's latent
345
+ into the merged model through the trained RecursiveLink."""
346
+ if not self.merge_available():
347
+ return ""
348
+ m = self._merged_model(alpha)
349
+ tok = self.toks["language"] # shared tokenizer
350
+ ids = self._ids("language", query); start = ids.shape[1]
351
+ ctx = int(getattr(m.config, "max_position_embeddings", 2048))
352
+ inj = None
353
+ if consult and getattr(self, "trained_link", None) is not None:
354
+ latent = self.models["math"](input_ids=self._ids("math", query)).latent
355
+ inj = self.trained_link(latent)
356
+ t = max(1e-5, temperature)
357
+ for _ in range(max_new):
358
+ logits = m(input_ids=ids[:, -ctx:], inject_latent=inj).logits[:, -1, :] / t
359
+ if top_k:
360
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
361
+ logits[logits < v[:, [-1]]] = -float("inf")
362
+ nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
363
+ ids = torch.cat([ids, nxt], dim=1)
364
+ if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
365
+ break
366
+ return tok.decode(ids[0, start:].tolist())
367
+
368
+
369
+ _MOE = None
370
+
371
+
372
+ def get_moe(device: str = "cpu"):
373
+ global _MOE
374
+ if _MOE is None:
375
+ _MOE = SpikeWhaleMoE(device=device)
376
+ else:
377
+ _MOE.reload()
378
+ return _MOE
379
+
380
+
381
+ if __name__ == "__main__":
382
+ moe = get_moe()
383
+ print("experts:", moe.available() or "(none trained yet)")
384
+ if moe.available():
385
+ for q in ["The mitochondria is the", "Solve for x: 2x + 3 =", "Book a flight to"]:
386
+ r = moe.run(q, max_new=60)
387
+ print(f"\nQ: {q!r}\n routed-> {r['winner']} bits/byte {r['bits_per_byte']} weights {r['weights']}")
388
+ print(f" gen: {r['generation']!r}")
agents/modmind/reasoning/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
agents/modmind/registry.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ registry.py -- the SINGLE SOURCE OF TRUTH for specialists.
3
+
4
+ To add a new specialist later, add ONE entry here, then:
5
+ python train_tokenizer.py --domains <name>
6
+ python train_specialist.py --domain <name> --muon --normuon --dean-schedule --compile
7
+ python train_link.py --asker language --consultant <name> # so it can be consulted
8
+
9
+ Every other script (tokenizer, data prep, model sizing, link trainer) reads from
10
+ here, so nothing else needs editing. `position` is the chain slot (context-doubling
11
+ / chain ordering in the V2 config); give each new specialist the next index.
12
+ """
13
+
14
+ # vocab=16384: the shared length-max tokenizer (covers prose + math; all 256 bytes). Both
15
+ # active specialists use the SAME tokenizer -> identical vocab makes routing directly comparable.
16
+ # PURE specialization (language=100% FineWeb-Edu, reasoning=100% FineMath) -> crisp routing.
17
+ SPECIALISTS = {
18
+ "language": dict(dataset="HuggingFaceFW/fineweb-edu", config="sample-100BT",
19
+ field="text", vocab=16384, position=0),
20
+ "reasoning": dict(dataset="HuggingFaceTB/finemath", config="finemath-3plus",
21
+ field="text", vocab=16384, position=1),
22
+ # dormant -- not in ACTIVE; ignore for now (no tool calling this round)
23
+ "tool_use": dict(dataset="glaiveai/glaive-function-calling-v2", config=None,
24
+ field=("system", "chat"), vocab=16384, position=2),
25
+ }
26
+
27
+ # which specialists you are actively training right now (the "foundation" set).
28
+ ACTIVE = ["language", "reasoning"]
29
+
30
+
31
+ def spec(name):
32
+ if name not in SPECIALISTS:
33
+ raise KeyError(f"unknown specialist {name!r}; add it to registry.SPECIALISTS. "
34
+ f"known: {list(SPECIALISTS)}")
35
+ return SPECIALISTS[name]
36
+
37
+
38
+ def text_of(name_or_spec, ex):
39
+ """Extract the training text from a streamed example (handles multi-field specs)."""
40
+ s = name_or_spec if isinstance(name_or_spec, dict) else spec(name_or_spec)
41
+ f = s["field"]
42
+ if isinstance(f, (tuple, list)):
43
+ return "\n".join(str(ex.get(k, "") or "") for k in f)
44
+ return ex.get(f, "") or ""
agents/modmind/specialist_presets.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ specialist_presets.py -- ModularMind-on-V2 specialist sizing.
3
+
4
+ DENSE ~80M specialists (Supra-50M-style, scaled up): a dense Llama-ish transformer with
5
+ NO MoE / Engram / Hyper-Connections / HRM, so the parameters go into language modeling
6
+ instead of machinery -> coherent generation (the lesson from SupraLabs/Supra-50M-Base,
7
+ a dense model that produces coherent multi-paragraph text on FineWeb-Edu).
8
+
9
+ Shape (shared across domains; only the vocab differs, read from registry.py):
10
+ hidden 640, 16 layers, 10 heads / 5 KV (GQA), dense FFN 1728, ctx 1024, d_latent 256.
11
+ -> ~81.5M params at vocab 16384 (the shared length-max tokenizer).
12
+
13
+ The bridge bus (d_latent=256) and latent IO are kept, so train_link.py / the Gradio
14
+ adapter still work after retraining.
15
+ """
16
+ from config import SpikeWhaleConfig
17
+ from registry import spec
18
+
19
+
20
+ def _dense_80m(vocab_size: int) -> SpikeWhaleConfig:
21
+ """A dense ~80M specialist for the given vocab."""
22
+ return SpikeWhaleConfig(
23
+ vocab_size=vocab_size,
24
+ hidden_size=640,
25
+ num_hidden_layers=16,
26
+ num_attention_heads=10,
27
+ num_key_value_heads=5, # GQA
28
+ head_dim=64,
29
+ qk_rope_head_dim=16,
30
+ q_lora_rank=320,
31
+ o_lora_rank=160,
32
+ tie_word_embeddings=True,
33
+ # DENSE: no MoE. moe_intermediate_size still sizes the DenseFFN (model.py).
34
+ use_moe=False,
35
+ moe_intermediate_size=1728,
36
+ # strip the heavy extras -> params go to the LM, not machinery
37
+ use_engram=False,
38
+ use_hyper_connections=False,
39
+ hc_mult=1,
40
+ use_hrm_refine=False,
41
+ num_nextn_predict_layers=0,
42
+ use_derf=False,
43
+ use_xsa=True,
44
+ # keep the ModularMind bridge bus so train_link.py / the adapter still work
45
+ use_latent_io=True,
46
+ d_latent=256,
47
+ # uniform 1024 context (Supra used 1024). base_context MUST be >= training --seq-len.
48
+ chain_position=0,
49
+ base_context=4096,
50
+ base_rope_theta=10000.0,
51
+ )
52
+
53
+
54
+ def specialist_config(domain: str = "language", position: int = 0) -> SpikeWhaleConfig:
55
+ """A dense ~80M specialist; vocab comes from registry.py (single source of truth)."""
56
+ return _dense_80m(spec(domain)["vocab"])
57
+
58
+
59
+ def generic_specialist_config(vocab_size: int, position: int = 0) -> SpikeWhaleConfig:
60
+ """Same dense ~80M shape for an arbitrary vocab (new domains 'just work')."""
61
+ return _dense_80m(vocab_size)
62
+
63
+
64
+ # Foundation chain ordering (derived from the registry, so it grows automatically)
65
+ try:
66
+ from registry import SPECIALISTS as _REG
67
+ FOUNDATION_ORDER = {v["position"]: k for k, v in _REG.items()}
68
+ except Exception:
69
+ FOUNDATION_ORDER = {0: "language", 1: "reasoning", 2: "tool_use"}
agents/modmind/spike_tokenizer.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ spike_tokenizer.py -- HuggingFace-compatible wrapper for the custom
3
+ byte-level "length-max" (greedy longest-match) tokenizer in tokenizer.json.
4
+
5
+ The raw tokenizer.json is NOT a HuggingFace `tokenizers` file; it is a plain
6
+ dict {vocab, vocab_size, max_token_len, algorithm:"length-max"}. This wrapper
7
+ makes it loadable by AutoTokenizer.from_pretrained / save_pretrained and
8
+ exposes encode/decode + the bos/eos/pad/unk ids the training scripts expect.
9
+
10
+ Encoding scheme (verified): byte-level. Text is UTF-8 encoded, each byte mapped
11
+ to its latin-1 character, then greedily matched against the vocab using the
12
+ longest key that matches at each position (max key length = max_token_len).
13
+ """
14
+ import json, os
15
+ from typing import List, Optional
16
+ from transformers import PreTrainedTokenizer
17
+
18
+
19
+ class SpikeTokenizer(PreTrainedTokenizer):
20
+ vocab_files_names = {"vocab_file": "tokenizer.json"}
21
+ model_input_names = ["input_ids"]
22
+
23
+ def __init__(self, vocab_file=None, **kwargs):
24
+ with open(vocab_file, "r", encoding="utf-8") as f:
25
+ data = json.load(f)
26
+ self._vocab = data["vocab"] # str -> id
27
+ self._ids_to_tokens = {i: t for t, i in self._vocab.items()}
28
+ self.max_token_len = int(data.get("max_token_len", 24))
29
+ # length-bucketed keys for fast greedy match (longest length first)
30
+ self._lengths = sorted({len(k) for k in self._vocab}, reverse=True)
31
+
32
+ kwargs.setdefault("bos_token", "<bos>")
33
+ kwargs.setdefault("eos_token", "<eos>")
34
+ kwargs.setdefault("unk_token", "<unk>")
35
+ kwargs.setdefault("pad_token", "<pad>")
36
+ super().__init__(**kwargs)
37
+
38
+ @property
39
+ def vocab_size(self) -> int:
40
+ return len(self._vocab)
41
+
42
+ def get_vocab(self):
43
+ return dict(self._vocab)
44
+
45
+ # --- core byte-level greedy tokenization ---
46
+ def _tokenize(self, text: str) -> List[str]:
47
+ s = text.encode("utf-8").decode("latin-1") # one char per byte
48
+ out, i, n = [], 0, len(s)
49
+ while i < n:
50
+ matched = None
51
+ hi = min(self.max_token_len, n - i)
52
+ for L in range(hi, 0, -1):
53
+ sub = s[i:i + L]
54
+ if sub in self._vocab:
55
+ matched = sub
56
+ break
57
+ if matched is None: # single byte always exists in vocab
58
+ matched = s[i]
59
+ out.append(matched)
60
+ i += len(matched)
61
+ return out
62
+
63
+ def _convert_token_to_id(self, token: str) -> int:
64
+ return self._vocab.get(token, self._vocab["<unk>"])
65
+
66
+ def _convert_id_to_token(self, index: int) -> str:
67
+ return self._ids_to_tokens.get(index, "<unk>")
68
+
69
+ def convert_tokens_to_string(self, tokens: List[str]) -> str:
70
+ specials = {"<pad>", "<unk>", "<bos>", "<eos>"}
71
+ byte_str = "".join(t for t in tokens if t not in specials)
72
+ return byte_str.encode("latin-1").decode("utf-8", errors="replace")
73
+
74
+ def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None):
75
+ os.makedirs(save_directory, exist_ok=True)
76
+ fn = (filename_prefix + "-" if filename_prefix else "") + "tokenizer.json"
77
+ path = os.path.join(save_directory, fn)
78
+ with open(path, "w", encoding="utf-8") as f:
79
+ json.dump({"vocab": self._vocab, "vocab_size": self.vocab_size,
80
+ "max_token_len": self.max_token_len,
81
+ "algorithm": "length-max"}, f, ensure_ascii=False)
82
+ return (path,)
agents/panel.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ panel.py -- the Gradio section for the bottom of the boss app: a live demo of the
3
+ Modular-Mind mixture-of-experts.
4
+
5
+ For the SpikeWhale backend it leads with the *latent bridge* (the real result) and
6
+ organizes the three demos into tabs. Every handler is a generator that yields an
7
+ instant "loading/generating" message first, so the first run never looks frozen while
8
+ the ~80M models lazy-load. Hot-reloads checkpoints.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import sys
14
+
15
+ import gradio as gr
16
+
17
+ EMOJI = {"language": "📖 Language", "math": "➗ Math", "tool": "🛠️ Tool-use"}
18
+ DEVICE = os.environ.get("MM_AGENTS_DEVICE", "cpu")
19
+ # Self-contained SpikeWhale bundle that ships next to this file (agents/modmind/: the 80M
20
+ # specialists + bridge + inference code). If it's present we default to the SpikeWhale backend
21
+ # so the HuggingFace Space "just works" with no env config. Env vars still override.
22
+ _BUNDLED_MODMIND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modmind")
23
+ _DEFAULT_BACKEND = "spikewhale" if os.path.isdir(_BUNDLED_MODMIND) else "bytegpt"
24
+ _SPIKEWHALE = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower() in ("spikewhale", "modmind")
25
+ _WARMED = {"done": False} # so the "loading the models" notice only shows on the first run
26
+
27
+ _FOOTER = (
28
+ "Two ~80M dense specialists — 📖 Language (FineWeb-Edu) and ➗ Math (FineMath) — sharing a "
29
+ "16k length-max tokenizer. A coordinator routes by bits-per-byte, and a trained RecursiveLink "
30
+ "lets them communicate in latent space (proven in the Bridge tab). Hot-reloads checkpoints."
31
+ if _SPIKEWHALE else
32
+ "Three byte-level ~10M specialists, streamed-trained on FineWeb-Edu / FineMath / "
33
+ "glaive-function-calling. Tiny + early-trained, so generations are rough — the routing "
34
+ "(which expert is most confident) is the point. It hot-reloads as training continues."
35
+ )
36
+
37
+
38
+ def _get_moe():
39
+ """Pick the MoE backend. Defaults to the bundled SpikeWhale 80M specialists
40
+ (agents/modmind/) when present, else the byte-level ByteGPT experts. MM_MOE_BACKEND
41
+ and MODMIND_DIR override."""
42
+ backend = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower()
43
+ if backend in ("spikewhale", "modmind"):
44
+ mm_dir = os.environ.get("MODMIND_DIR", _BUNDLED_MODMIND)
45
+ if mm_dir and mm_dir not in sys.path:
46
+ sys.path.insert(0, mm_dir) # front: ModMind's model.py wins over agents/model.py
47
+ from moe_gradio import get_moe
48
+ return get_moe
49
+ from orchestrator import get_moe
50
+ return get_moe
51
+
52
+
53
+ def _loading_notice(action="Generating"):
54
+ """First-run popup + in-place message so nothing ever looks frozen."""
55
+ if not _WARMED["done"]:
56
+ gr.Info("First run — loading the models (~20–40s on CPU). After this, it's quick.")
57
+ return (f"### ⏳ Loading the models + {action.lower()}…\n"
58
+ "*First run loads the ~80M specialists into memory — this can take ~20–40s on CPU. "
59
+ "Every run after this is fast.*")
60
+ return f"### ⏳ {action}…"
61
+
62
+
63
+ def _bar(frac, n=18):
64
+ f = max(0.0, min(1.0, frac))
65
+ return "█" * round(f * n) + "·" * (n - round(f * n))
66
+
67
+
68
+ def moe_run(query, max_new):
69
+ yield _loading_notice("Routing & generating")
70
+ moe = _get_moe()(DEVICE)
71
+ if not moe.available():
72
+ if _SPIKEWHALE:
73
+ yield ("### ⏳ No SpikeWhale experts found\nSet `MODMIND_DIR` to your ModMind folder "
74
+ "and make sure `<domain>/checkpoints/step_*.pt` exist (the panel hot-reloads them).")
75
+ else:
76
+ yield ("### ⏳ No experts trained yet\nRun `python agents/train.py --expert language` "
77
+ "(and `math`, `tool`).")
78
+ return
79
+ q = (query or "").strip() or "The"
80
+ r = moe.run(q, max_new=int(max_new))
81
+ _WARMED["done"] = True
82
+ w = r["weights"]; bits = r["bits_per_byte"]; steps = r["steps"]
83
+ rows = "\n".join(
84
+ f"| {EMOJI.get(n, n)} | {steps.get(n,0):,} | {bits[n]:.2f} | `{_bar(w[n])}` {w[n]*100:4.1f}% |"
85
+ + (" ⬅ **routed**" if n == r["winner"] else "")
86
+ for n in w
87
+ )
88
+ lat = r["shared_latent"][:16]
89
+ spark = "".join("▁▂▃▄▅▆▇█"[min(7, int((abs(v)) / (max(1e-3, max(abs(x) for x in lat))) * 7))] for v in lat)
90
+ yield f"""### Routed to {EMOJI.get(r['winner'], r['winner'])}
91
+ The expert with the **lowest bits/byte** (most fluent on your text) wins the route.
92
+
93
+ | expert | train steps | bits/byte | routing weight |
94
+ |---|---|---|---|
95
+ {rows}
96
+
97
+ **{EMOJI.get(r['winner'], r['winner'])} continues your prompt:**
98
+ > {q}**{r['generation']}**
99
+
100
+ **Fused latent** (both experts' output latents combined — a glimpse of the shared bus): `{spark}`
101
+
102
+ <sub>{_FOOTER}</sub>"""
103
+
104
+
105
+ def moe_key_recall(n):
106
+ """THE PROOF: a random key shown only to the consultant; the asker reproduces it from the
107
+ latent alone (with) vs ablated (without)."""
108
+ yield _loading_notice("Running the proof")
109
+ moe = _get_moe()(DEVICE)
110
+ if not getattr(moe, "key_recall_available", lambda: False)():
111
+ yield ("### 🔑 Bridge unavailable\nNeeds the **SpikeWhale** backend and a trained "
112
+ "`links/<asker>__from__<consultant>.pt` saved with the full asker.")
113
+ return
114
+ meta = moe.consult_meta()
115
+ a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
116
+ wr = moe.key_recall(n=int(n), ablate=False)
117
+ ar = moe.key_recall(n=int(n), ablate=True)
118
+ _WARMED["done"] = True
119
+ rows = "\n".join(f"| `{k}` | `{rec}` | {'✅' if ok else '❌'} |" for k, rec, ok in wr["examples"])
120
+ return_ok = wr['acc'] * 100
121
+ return_no = ar['acc'] * 100
122
+ yield f"""### 🔑 {a} read {c}'s mind — {return_ok:.0f}% with the latent, {return_no:.0f}% without
123
+
124
+ A random key is shown **only to {c}**. {a} never sees it — yet by reading {c}'s latent through the
125
+ trained RecursiveLink, it reproduces the key:
126
+
127
+ | secret key (only {c} saw it) | {a} recovered it | |
128
+ |---|---|---|
129
+ {rows}
130
+
131
+ ## ✅ With the latent: {return_ok:.0f}% → ❌ Cut the latent: {return_no:.0f}%
132
+
133
+ <sub>That collapse to chance when the latent is removed is the whole point: the information is genuinely
134
+ crossing the latent bridge between two models that were trained **separately, on different data**.
135
+ This is the result — routing and generation are the supporting act.</sub>"""
136
+
137
+
138
+ def moe_consult(query, max_new):
139
+ """Latent-influence demo: inject the consultant's latent into the asker (WITH vs WITHOUT).
140
+ The bridge was trained on key-recall, so the latent *steers* the output rather than answering."""
141
+ yield _loading_notice("Injecting latent")
142
+ moe = _get_moe()(DEVICE)
143
+ if not getattr(moe, "consult_available", lambda: False)():
144
+ yield ("### 🔗 Latent bridge unavailable\nNeeds the **SpikeWhale** backend and a trained bridge.")
145
+ return
146
+ q = (query or "").strip() or "natural selection"
147
+ meta = moe.consult_meta()
148
+ a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
149
+ with_gen = moe.consult(q, max_new=int(max_new), ablate=False)
150
+ abl_gen = moe.consult(q, max_new=int(max_new), ablate=True)
151
+ _WARMED["done"] = True
152
+ wl, nl = meta["with_latent"], meta["without_latent"]
153
+ yield f"""### 🔗 Latent influence — {c}'s latent injected into {a}
154
+
155
+ **WITH {c}'s latent (through the trained RecursiveLink):**
156
+ > {q}**{with_gen}**
157
+
158
+ **WITHOUT it (latent ablated to zero):**
159
+ > {q}**{abl_gen}**
160
+
161
+ <sub>This shows the latent's raw *effect* on generation, not a Q&A answer. The bridge was trained on
162
+ key-recall (ablation with={wl:.3f} vs without={nl:.4f}), so the injected latent pushes the asker toward
163
+ that learned behavior — which is why WITH and WITHOUT differ so sharply. The clean proof the latent
164
+ carries real information is the **Bridge** tab.</sub>"""
165
+
166
+
167
+ def moe_combine(query, max_new, blend, consult):
168
+ """Two blends compared at the same mix ratio: a real WEIGHT-MERGE (one merged model) vs an
169
+ OUTPUT-BLEND (two models run separately, distributions averaged)."""
170
+ yield _loading_notice("Building merge + blending")
171
+ moe = _get_moe()(DEVICE)
172
+ if not getattr(moe, "merge_available", lambda: False)():
173
+ yield "### 🧬 Needs both specialists loaded."
174
+ return
175
+ q = (query or "").strip() or "The water cycle works by"
176
+ a = float(blend)
177
+ merged_gen = moe.merge_generate(q, alpha=a, max_new=int(max_new), consult=bool(consult))
178
+ blend_gen = moe.combine(q, max_new=int(max_new), blend=a, consult=bool(consult))
179
+ _WARMED["done"] = True
180
+ mix = f"{int(round((1-a)*100))}% 📖 Language / {int(round(a*100))}% ➗ Math"
181
+ extra = " · +Reasoning's latent (consult)" if consult else ""
182
+ yield f"""### 🧬 MoE Modular Minds — two ways to blend · {mix}{extra}
183
+
184
+ **① Weight merge** — a *single* model whose weights are `(1-α)·Language + α·Math` — one network, one forward pass:
185
+ > {q}**{merged_gen}**
186
+
187
+ **② Output blend** — both models run separately, their next-token distributions averaged each step:
188
+ > {q}**{blend_gen}**
189
+
190
+ <sub>Same mix ratio, two different mechanisms. **Weight merge** fuses the actual parameters into one model
191
+ (only possible because they're the identical dense architecture); **output blend** is an inference-time
192
+ ensemble of two separate models (only possible because they share the 16k tokenizer). Tick *consult* to also
193
+ route Reasoning's latent into each through the trained bridge. Exploratory — generations are rough at this scale.</sub>"""
194
+
195
+
196
+ HERO = """# 🧩 Modular Mind — two specialists that talk in latent space
197
+ **Two ~80M models trained completely separately** — 📖 **Language** on FineWeb-Edu, ➗ **Math** on
198
+ FineMath — that never saw each other's data. A coordinator **routes** your query to the right one,
199
+ and a trained **RecursiveLink** lets them **communicate through latent space**: Language can read
200
+ information straight out of Math's "mind." The **🔑 Bridge** tab proves it.
201
+
202
+ > ℹ️ *These specialists were trained only to demonstrate a **verifiable result** — clean routing and a
203
+ > provable latent-bridge ablation — **not** for production-quality output. The generated text is
204
+ > intentionally rough at this scale; the mechanism is the point.*"""
205
+
206
+ BRIDGE_INTRO = """### The proof: two independent models, one latent channel
207
+ A random secret key is shown **only to ➗ Math**. 📖 Language never sees it — but by reading Math's
208
+ latent through the trained RecursiveLink, it **reproduces the key**. Zero out the latent and it
209
+ collapses to chance. That gap *is* the result: real information crossing between two models that
210
+ were trained on different data and never met. **Hit the button.**"""
211
+
212
+ INTRO_BYTE = """## 🧩 Experiment — Modular Mind as a Mixture of Experts
213
+ Three tiny ~10M byte-level specialists (language, math, tool-use), each streamed-trained on its own
214
+ dataset. A coordinator **routes** your query to whichever expert is most fluent (perplexity-based MoE)
215
+ and fuses their latents through a **RecursiveLink**. Try a math problem vs. a sentence."""
216
+
217
+
218
+ def _routing_block():
219
+ with gr.Row():
220
+ q = gr.Textbox(label="Your prompt", value="Solve for x: 2x + 3 = 11",
221
+ scale=4, placeholder="a sentence or a math problem…")
222
+ n = gr.Slider(40, 300, value=80, step=20, label="generate tokens", scale=1)
223
+ btn = gr.Button("🧭 Route & generate", variant="primary")
224
+ out = gr.Markdown()
225
+ btn.click(moe_run, [q, n], out)
226
+ gr.Examples(examples=[["The theory of evolution explains", 80],
227
+ ["Compute the derivative of x^2 + 3x", 80],
228
+ ["The history of the Roman Empire began", 80]],
229
+ inputs=[q, n])
230
+
231
+
232
+ def build_moe_panel():
233
+ """Create the MoE demo components inside the current gr.Blocks context."""
234
+ if not _SPIKEWHALE:
235
+ with gr.Accordion("🧩 Experiment: Modular Mind = Mixture of Experts (3 specialists)", open=False):
236
+ gr.Markdown(INTRO_BYTE)
237
+ _routing_block()
238
+ return
239
+
240
+ with gr.Accordion("🧩 Modular Mind — independent specialists communicating in latent space", open=True):
241
+ gr.Markdown(HERO)
242
+ with gr.Tabs():
243
+ # The headline result, FIRST.
244
+ with gr.Tab("🔑 The latent bridge — the proof"):
245
+ gr.Markdown(BRIDGE_INTRO)
246
+ with gr.Row():
247
+ kn = gr.Slider(4, 16, value=8, step=1, label="keys to test", scale=3)
248
+ kbtn = gr.Button("🔑 Run the proof", variant="primary", scale=1)
249
+ kout = gr.Markdown()
250
+ kbtn.click(moe_key_recall, [kn], kout)
251
+
252
+ # Routing — the supporting act.
253
+ with gr.Tab("🧭 Routing & generation"):
254
+ gr.Markdown("Type a math problem vs. a sentence and watch the **route flip** — each "
255
+ "expert is most fluent (lowest bits/byte) on its own domain.")
256
+ _routing_block()
257
+
258
+ # Latent influence — honest "with vs without".
259
+ with gr.Tab("🔗 Latent influence"):
260
+ gr.Markdown(
261
+ "Inject ➗ Math's latent into 📖 Language through the **trained RecursiveLink** and "
262
+ "see generation **with vs without** it. The bridge was trained on key-recall, so the "
263
+ "latent *steers* the output (it doesn't answer the question) — the point is how "
264
+ "strongly it changes generation. The clean proof is the **Bridge** tab.")
265
+ with gr.Row():
266
+ cq = gr.Textbox(label="Text to encode (Math's latent → injected into Language)",
267
+ value="natural selection", scale=4)
268
+ cn = gr.Slider(40, 200, value=80, step=20, label="generate tokens", scale=1)
269
+ cbtn = gr.Button("🔗 Show latent influence (with vs without)", variant="secondary")
270
+ cout = gr.Markdown()
271
+ cbtn.click(moe_consult, [cq, cn], cout)
272
+
273
+ # MoE Modular Minds — TWO ways to blend the specialists, compared side by side.
274
+ with gr.Tab("🧬 MoE Modular Minds"):
275
+ gr.Markdown(
276
+ "**Two ways to blend the two specialists**, shown side by side at the same mix ratio:\n"
277
+ "- **① Weight merge** — fuse the *parameters* into one model `(1-α)·Language + α·Math` "
278
+ "(works because they're the identical dense architecture).\n"
279
+ "- **② Output blend** — run both models separately and average their next-token "
280
+ "distributions (works because they share the 16k tokenizer).\n\n"
281
+ "Slide the mix, and tick *consult* to also route Reasoning's latent into each through the "
282
+ "trained bridge.")
283
+ with gr.Row():
284
+ mq = gr.Textbox(label="Prompt", value="The water cycle works by", scale=4)
285
+ mn = gr.Slider(40, 160, value=70, step=10, label="generate tokens", scale=1)
286
+ with gr.Row():
287
+ mblend = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
288
+ label="mix α: 0 = 📖 Language ⟷ 1 = ➗ Math", scale=3)
289
+ mconsult = gr.Checkbox(value=False, label="consult (inject Reasoning's latent)", scale=1)
290
+ mbtn = gr.Button("🧬 Blend both ways (weight-merge vs output-blend)", variant="primary")
291
+ mout = gr.Markdown()
292
+ mbtn.click(moe_combine, [mq, mn, mblend, mconsult], mout)
app.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py -- Modular Mind: Boss Fight (HuggingFace Space entry point).
3
+
4
+ A 2D Dark-Souls-style duel. The boss (Demon Slime) is driven by a tiny Modular
5
+ Mind: six specialist networks emit latents that a RecursiveLink merges into one
6
+ shared latent, and a coordinator reads it to pick the boss's next move. The brain
7
+ was trained by self-play reinforcement learning (see train.py / duel_sim.py).
8
+
9
+ The browser renders the fight at 60fps; at each decision point it calls the Python
10
+ brain through this app's /decide endpoint and shows the Modular Mind deciding live.
11
+ """
12
+ import json
13
+ import os
14
+ import sys
15
+ from urllib.parse import quote
16
+
17
+ import gradio as gr
18
+
19
+ import modular_mind
20
+ import online
21
+
22
+ # the MoE-experts experiment lives in ./agents (added to the bottom of the page)
23
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "agents"))
24
+ try:
25
+ from panel import build_moe_panel
26
+ except Exception as _e: # agents optional -> game still runs without it
27
+ build_moe_panel = None
28
+ print(f"[app] MoE experiment panel unavailable ({_e})")
29
+
30
+ HERE = os.path.dirname(os.path.abspath(__file__))
31
+
32
+ # the self-playing piano (a Modular Mind trained on a song) lives in ./piano
33
+ sys.path.insert(0, os.path.join(HERE, "piano"))
34
+ _PIANO = {"player": None, "tried": False}
35
+
36
+
37
+ def _get_piano():
38
+ if not _PIANO["tried"]:
39
+ _PIANO["tried"] = True
40
+ try:
41
+ from poly_mind import PolyPlayer
42
+ _PIANO["player"] = PolyPlayer()
43
+ except Exception as e:
44
+ print(f"[app] piano Modular Mind unavailable ({e})")
45
+ return _PIANO["player"]
46
+
47
+
48
+ try:
49
+ _pmeta = json.load(open(os.path.join(HERE, "piano", "poly_notes.json")))
50
+ PIANO_LO, PIANO_HI, PIANO_FPS = _pmeta["midi_lo"], _pmeta["midi_hi"], _pmeta.get("fps", 8)
51
+ except Exception:
52
+ PIANO_LO, PIANO_HI, PIANO_FPS = 56, 86, 8
53
+
54
+ _get_piano() # warm the piano Modular Mind at app startup (so the first play is instant)
55
+
56
+
57
+ def _read(path):
58
+ with open(path, "r", encoding="utf-8") as f:
59
+ return f.read()
60
+
61
+
62
+ CSS = _read(os.path.join(HERE, "web", "game.css"))
63
+ GAME_JS = _read(os.path.join(HERE, "web", "game.js"))
64
+ ASSETS_JS = _read(os.path.join(HERE, "assets_data.js"))
65
+ INDEX_HTML = _read(os.path.join(HERE, "web", "index.html"))
66
+
67
+ # music/sfx are served as static files by Gradio (allowed_paths below); the game
68
+ # builds audio URLs from this base.
69
+ AUDIO_DIR = os.path.join(HERE, "audio")
70
+ # URL-encode the absolute path (it may contain spaces) but keep "/" and the drive ":"
71
+ AUDIO_BASE_URL = (
72
+ "/gradio_api/file=" + quote(AUDIO_DIR.replace(os.sep, "/"), safe="/:") + "/"
73
+ )
74
+
75
+ # real acoustic-grand-piano note samples (served static; the piano plays the nearest
76
+ # sample pitch-shifted to each note, for a real piano sound instead of an oscillator).
77
+ PIANO_SAMPLES_DIR = os.path.join(HERE, "piano", "samples")
78
+ try:
79
+ PIANO_SAMPLE_MIDIS = sorted(int(f[:-4]) for f in os.listdir(PIANO_SAMPLES_DIR) if f.endswith(".mp3"))
80
+ except Exception:
81
+ PIANO_SAMPLE_MIDIS = []
82
+ PIANO_SAMPLE_BASE = "/gradio_api/file=" + quote(PIANO_SAMPLES_DIR.replace(os.sep, "/"), safe="/:") + "/"
83
+
84
+ # warm the default brain
85
+ modular_mind.get_mind("hard")
86
+
87
+
88
+ def decide(state_json: str) -> str:
89
+ """Called by the browser at each boss decision point. In: game-state JSON
90
+ (includes a "difficulty" tier). Out: chosen action + telemetry, as JSON."""
91
+ try:
92
+ state = json.loads(state_json)
93
+ except Exception:
94
+ state = {}
95
+ return json.dumps(modular_mind.decide(state))
96
+
97
+
98
+ def learn(traj_json: str) -> str:
99
+ """Called by the browser at the end of a fight with the full decision trajectory
100
+ + outcome. Buffers it and periodically finetunes the HARD brain (REINFORCE)."""
101
+ try:
102
+ traj = json.loads(traj_json)
103
+ except Exception:
104
+ return json.dumps({"error": "bad json"})
105
+ return json.dumps(online.record_fight(traj))
106
+
107
+
108
+ def piano(payload_json: str) -> str:
109
+ """Called by the browser's self-playing piano: in = {history:[tokens], n}, out =
110
+ {notes:[midi...], history:[...]}. The Modular Mind autoregressively generates the
111
+ next `n` notes from the recent history (server-side; history kept client-side)."""
112
+ try:
113
+ req = json.loads(payload_json)
114
+ except Exception:
115
+ req = {}
116
+ player = _get_piano()
117
+ hist = list(req.get("history") or [])
118
+ n = max(1, min(64, int(req.get("n", 32))))
119
+ if player is None:
120
+ return json.dumps({"notes": [], "history": hist, "error": "piano unavailable"})
121
+ if not hist:
122
+ hist = [list(f) for f in player.seed]
123
+ frames, telem = [], []
124
+ for _ in range(n):
125
+ toks, midis, tl = player.next_frame(hist)
126
+ hist.append(toks); frames.append([int(x) for x in midis]); telem.append(tl)
127
+ return json.dumps({"frames": frames, "telem": telem,
128
+ "history": [list(map(int, f)) for f in hist[-player.K:]]})
129
+
130
+
131
+ # Bootstrap (runs in the browser): wire window.MM_DECIDE to this app's /decide
132
+ # endpoint via Gradio's REST API (no external CDN), then boot the game once the
133
+ # gr.HTML canvas is in the DOM.
134
+ BOOTSTRAP_JS = """
135
+ (function () {
136
+ // route each boss decision to the Python Modular Mind through /gradio_api/call
137
+ window.MM_DECIDE = async (state) => {
138
+ const post = await fetch('/gradio_api/call/decide', {
139
+ method: 'POST', headers: {'Content-Type': 'application/json'},
140
+ body: JSON.stringify({data: [JSON.stringify(state)]}),
141
+ });
142
+ const j = await post.json();
143
+ const res = await fetch('/gradio_api/call/decide/' + j.event_id);
144
+ const text = await res.text();
145
+ const line = text.split('\\n').filter(l => l.startsWith('data:')).pop();
146
+ const arr = JSON.parse(line.slice(5).trim());
147
+ return JSON.parse(arr[0]);
148
+ };
149
+ // send a finished fight's trajectory to the online learner (fire-and-forget)
150
+ window.MM_LEARN = async (traj) => {
151
+ try {
152
+ const post = await fetch('/gradio_api/call/learn', {
153
+ method: 'POST', headers: {'Content-Type': 'application/json'},
154
+ body: JSON.stringify({data: [JSON.stringify(traj)]}),
155
+ });
156
+ const j = await post.json();
157
+ await fetch('/gradio_api/call/learn/' + j.event_id);
158
+ } catch (e) { /* learning is best-effort */ }
159
+ };
160
+ const tryBoot = () => {
161
+ if (document.getElementById('mm-canvas') && window.__mmBoot) window.__mmBoot();
162
+ else setTimeout(tryBoot, 80);
163
+ };
164
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryBoot);
165
+ else tryBoot();
166
+ })();
167
+ """
168
+
169
+ # Force Gradio dark mode (matches the dark game) regardless of the visitor's browser
170
+ # setting, by ensuring the ?__theme=dark URL param is present before the app renders.
171
+ FORCE_DARK_JS = """
172
+ (function () {
173
+ try {
174
+ var p = new URLSearchParams(window.location.search);
175
+ if (p.get('__theme') !== 'dark') {
176
+ p.set('__theme', 'dark');
177
+ window.location.replace(window.location.pathname + '?' + p.toString() + window.location.hash);
178
+ }
179
+ } catch (e) {}
180
+ })();
181
+ """
182
+
183
+ # ---- self-playing piano (a Modular Mind trained on the song) -----------------
184
+ PIANO_CSS = """
185
+ #mm-piano-wrap{max-width:920px;margin:6px auto 2px;font-family:system-ui,sans-serif}
186
+ #mm-piano{display:flex;align-items:flex-end;justify-content:center;gap:2px;height:120px;
187
+ background:#13131a;border:1px solid #2a2a35;border-radius:8px;padding:10px 8px;overflow-x:auto}
188
+ .pk{box-sizing:border-box;border:1px solid #05050a;border-radius:0 0 3px 3px;flex:0 0 auto}
189
+ .pk.white{width:20px;height:100px;background:#e9e9ee}
190
+ .pk.black{width:14px;height:62px;background:#2b2b33}
191
+ .pk.on.white{background:#7ad1ff;box-shadow:0 0 14px #7ad1ff}
192
+ .pk.on.black{background:#3ba0d8;box-shadow:0 0 14px #3ba0d8}
193
+ #mm-piano-ctrl{display:flex;gap:14px;align-items:center;justify-content:center;margin:8px auto 4px}
194
+ #mm-piano-btn{cursor:pointer;background:#2a9d6a;color:#fff;border:none;border-radius:6px;
195
+ padding:9px 18px;font-weight:700;font-size:14px}
196
+ #mm-piano-btn:hover{background:#33b87c}
197
+ #mm-piano-note{color:#9bd;font-size:13px;min-width:96px;text-align:left}
198
+ #mm-piano-specs{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:8px auto 2px;max-width:800px}
199
+ .psp{width:110px;background:#16161e;border:1px solid #2a2a35;border-radius:6px;padding:6px 9px}
200
+ .psp .nm{font-weight:700;font-size:12px}
201
+ .psp .ow{opacity:.55;font-size:10px;color:#aaa;margin-top:1px}
202
+ .psp .bar{height:7px;background:#2a2a33;border-radius:4px;margin-top:6px;overflow:hidden}
203
+ .psp .fill{height:100%;width:4%;border-radius:4px;transition:width .12s ease}
204
+ #mm-piano-lbl{text-align:center;color:#888;font-size:11px;margin-top:8px}
205
+ #mm-piano-latent{display:flex;gap:3px;justify-content:center;align-items:flex-end;height:22px;margin:5px auto}
206
+ #mm-piano-latent .lc{width:8px;height:3px;border-radius:2px;background:#3a3a48;transition:height .12s ease}
207
+ """
208
+ PIANO_GLOBALS = (f"window.MM_PIANO_LO={PIANO_LO};window.MM_PIANO_HI={PIANO_HI};"
209
+ f"window.MM_PIANO_FPS={PIANO_FPS};"
210
+ f"window.MM_PIANO_SAMPLE_BASE={json.dumps(PIANO_SAMPLE_BASE)};"
211
+ f"window.MM_PIANO_SAMPLE_MIDIS={json.dumps(PIANO_SAMPLE_MIDIS)};")
212
+ PIANO_JS = r"""
213
+ (function(){
214
+ var SPC={Bass:'#4da6ff',Tenor:'#2ecc71',Soprano:'#ff6b9d',Sustain:'#1abc9c',Rest:'#95a5a6',Onset:'#e67e22',Phrase:'#9b59b6'};
215
+ window.__pianoBoot = function(){
216
+ var wrap=document.getElementById('mm-piano');
217
+ if(!wrap || wrap.dataset.built) return; wrap.dataset.built='1';
218
+ var LO=window.MM_PIANO_LO||56, HI=window.MM_PIANO_HI||86, BLACK={1:1,3:1,6:1,8:1,10:1};
219
+ for(var m=LO;m<=HI;m++){var k=document.createElement('div');
220
+ k.className='pk '+(BLACK[m%12]?'black':'white'); k.id='pk-'+m; wrap.appendChild(k);}
221
+ var audio=null, playing=false, queue=[], history=[], fetching=false, timer=null, voices={};
222
+ var specFills={}, built=false, buffers={}, loaded=false;
223
+ var PLAY_MS=Math.round(1000/(window.MM_PIANO_FPS||8))+58; // a touch slower = calmer feel
224
+ var noteEl=document.getElementById('mm-piano-note'), btn=document.getElementById('mm-piano-btn');
225
+ var specBox=document.getElementById('mm-piano-specs'), latBox=document.getElementById('mm-piano-latent');
226
+ var NN=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
227
+ function name(m){return NN[m%12]+(Math.floor(m/12)-1);}
228
+ function buildSpecs(telem){
229
+ if(built || !specBox || !telem) return; built=true;
230
+ telem.spec.forEach(function(s){
231
+ var c=SPC[s.name]||'#888', card=document.createElement('div'); card.className='psp';
232
+ card.innerHTML='<div class="nm" style="color:'+c+'">'+s.name+'</div>'+
233
+ '<div class="ow">'+(s.owns?('owns '+s.owns):'modulator')+'</div>'+
234
+ '<div class="bar"><div class="fill" style="background:'+c+'"></div></div>';
235
+ specBox.appendChild(card); specFills[s.name]=card.querySelector('.fill');
236
+ });
237
+ if(latBox){ for(var i=0;i<8;i++){var lc=document.createElement('div'); lc.className='lc'; latBox.appendChild(lc);} }
238
+ }
239
+ function updateSpecs(telem){
240
+ if(!telem) return; buildSpecs(telem);
241
+ telem.spec.forEach(function(s){
242
+ var h;
243
+ if(s.owns!=null && s.drive!=null){ h=Math.abs(s.drive)/4.0*100; } // owners: by drive
244
+ else { h=(s.act-16.0)/10.0*100; } // modulators: by latent pulse
245
+ h=Math.max(4,Math.min(100,h));
246
+ if(specFills[s.name]) specFills[s.name].style.width=h+'%';
247
+ });
248
+ if(latBox && telem.shared){ var lc=latBox.children;
249
+ for(var i=0;i<lc.length && i<telem.shared.length;i++){
250
+ lc[i].style.height=Math.max(2,Math.min(20,Math.abs(telem.shared[i])*9))+'px';
251
+ lc[i].style.background=telem.shared[i]>=0?'#5bbcdf':'#df7a5b';
252
+ } }
253
+ }
254
+ async function fetchPhrase(){
255
+ if(fetching) return; fetching=true;
256
+ try{
257
+ var post=await fetch('/gradio_api/call/piano',{method:'POST',
258
+ headers:{'Content-Type':'application/json'},
259
+ body:JSON.stringify({data:[JSON.stringify({history:history,n:32})]})});
260
+ var j=await post.json();
261
+ var res=await fetch('/gradio_api/call/piano/'+j.event_id);
262
+ var text=await res.text();
263
+ var line=text.split('\n').filter(function(l){return l.indexOf('data:')===0;}).pop();
264
+ var out=JSON.parse(JSON.parse(line.slice(5).trim())[0]);
265
+ history=out.history||history;
266
+ var fr=out.frames||[]; for(var i=0;i<fr.length;i++) queue.push({f:fr[i], t:(out.telem&&out.telem[i])||null});
267
+ }catch(e){}
268
+ fetching=false;
269
+ }
270
+ function loadSamples(){
271
+ if(loaded || !audio) return; loaded=true; // background load; play() upgrades to samples as they arrive
272
+ var ms=window.MM_PIANO_SAMPLE_MIDIS||[], base=window.MM_PIANO_SAMPLE_BASE||'';
273
+ ms.forEach(function(sm){
274
+ var ctl=('AbortController' in window)?new AbortController():null;
275
+ var to=ctl?setTimeout(function(){ctl.abort();},8000):0;
276
+ fetch(base+sm+'.mp3', ctl?{signal:ctl.signal}:{}).then(function(r){return r.arrayBuffer();})
277
+ .then(function(ab){audio.decodeAudioData(ab,function(buf){buffers[sm]=buf;},function(){});})
278
+ .catch(function(){}).finally(function(){if(to)clearTimeout(to);});
279
+ });
280
+ }
281
+ function nearest(m){ var ks=Object.keys(buffers); if(!ks.length) return null;
282
+ return ks.map(Number).reduce(function(a,b){return Math.abs(b-m)<Math.abs(a-m)?b:a;}); }
283
+ function voice(m, vol){ // real sample if it's loaded, else an oscillator -> ALWAYS audible
284
+ if(!audio) return null;
285
+ var sm=nearest(m), t=audio.currentTime;
286
+ if(sm!=null && buffers[sm]){
287
+ var src=audio.createBufferSource(); src.buffer=buffers[sm];
288
+ src.playbackRate.value=Math.pow(2,(m-sm)/12);
289
+ var g=audio.createGain(); g.gain.value=vol;
290
+ src.connect(g); g.connect(audio.destination); src.start(t);
291
+ return {src:src, gain:g};
292
+ }
293
+ var f=440*Math.pow(2,(m-69)/12);
294
+ var o1=audio.createOscillator(); o1.type='triangle'; o1.frequency.value=f;
295
+ var o2=audio.createOscillator(); o2.type='sine'; o2.frequency.value=f*2;
296
+ var g2=audio.createGain(); g2.gain.value=0.18;
297
+ var lp=audio.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=2600;
298
+ var g=audio.createGain();
299
+ g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol,t+0.014);
300
+ g.gain.exponentialRampToValueAtTime(Math.max(0.0001,vol*0.3),t+1.6);
301
+ o1.connect(lp); o2.connect(g2); g2.connect(lp); lp.connect(g); g.connect(audio.destination);
302
+ o1.start(t); o2.start(t);
303
+ return {oscs:[o1,o2], gain:g};
304
+ }
305
+ function releaseNode(nd){
306
+ if(!nd || !audio) return; var t=audio.currentTime;
307
+ try{ nd.gain.gain.cancelScheduledValues(t);
308
+ nd.gain.gain.setValueAtTime(Math.max(nd.gain.gain.value,0.0001),t);
309
+ nd.gain.gain.linearRampToValueAtTime(0.0001,t+0.10);
310
+ if(nd.src) nd.src.stop(t+0.13); if(nd.oscs) nd.oscs.forEach(function(o){o.stop(t+0.13);});
311
+ }catch(e){}
312
+ }
313
+ function releaseAll(){
314
+ for(var mk in voices){ releaseNode(voices[mk]); var pe=document.getElementById('pk-'+mk); if(pe)pe.classList.remove('on'); }
315
+ voices={};
316
+ }
317
+ function playFrame(midis){ // polyphony: strike new notes, hold sustained ones, release dropped ones
318
+ var nw={}; (midis||[]).forEach(function(m){ if(m>0) nw[m]=1; });
319
+ for(var mk in voices){ if(!nw[mk]){ releaseNode(voices[mk]);
320
+ var pe=document.getElementById('pk-'+mk); if(pe)pe.classList.remove('on'); delete voices[mk]; } }
321
+ var on=Object.keys(nw), vol=on.length>2?0.5:0.65;
322
+ on.forEach(function(ms){ var m=+ms; if(!voices[m]){ var v=voice(m,vol); if(v) voices[m]=v;
323
+ var el=document.getElementById('pk-'+m); if(el)el.classList.add('on'); } });
324
+ if(noteEl){ noteEl.textContent= on.length ? ('♪ '+on.map(function(ms){return name(+ms);}).join(' ')) : '♪ (rest)'; }
325
+ }
326
+ function tick(){ if(!playing) return; if(queue.length<10 && !fetching) fetchPhrase();
327
+ if(queue.length>0){ var it=queue.shift(); playFrame(it.f); updateSpecs(it.t); } }
328
+ function start(){
329
+ if(!audio) audio=new (window.AudioContext||window.webkitAudioContext)();
330
+ if(audio.state==='suspended'){ try{audio.resume();}catch(e){} }
331
+ loadSamples(); // real piano loads in background; oscillator plays until then
332
+ playing=true; btn.textContent='⏸ Pause';
333
+ if(queue.length===0) fetchPhrase();
334
+ if(!timer) timer=setInterval(tick, PLAY_MS);
335
+ }
336
+ function stop(){ playing=false; btn.textContent='▶ Let the Modular Mind play'; releaseAll(); }
337
+ btn.onclick=function(){ playing?stop():start(); };
338
+ };
339
+ var t=function(){ if(document.getElementById('mm-piano')) window.__pianoBoot(); else setTimeout(t,120); };
340
+ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',t); else t();
341
+ })();
342
+ """
343
+ PIANO_HTML = """
344
+ <div id="mm-piano-wrap">
345
+ <div id="mm-piano"></div>
346
+ <div id="mm-piano-ctrl">
347
+ <button id="mm-piano-btn">▶ Let the Modular Mind play</button>
348
+ <span id="mm-piano-note">♪</span>
349
+ </div>
350
+ <div id="mm-piano-lbl">specialists firing for each chord — Bass / Tenor / Soprano own a register; Sustain / Onset / Phrase are modulators that only write to the shared latent</div>
351
+ <div id="mm-piano-specs"></div>
352
+ <div id="mm-piano-latent" title="RecursiveLink shared latent"></div>
353
+ </div>
354
+ """
355
+
356
+ # Injected verbatim into the page <head>: dark-mode forcer, stylesheet, embedded sprite
357
+ # atlases, the game engine, the piano engine, and the bootstrap. (Inline <script> in <head>
358
+ # runs reliably; gr.HTML's innerHTML scripts do not.)
359
+ HEAD = (
360
+ f"<script>{FORCE_DARK_JS}</script>\n"
361
+ f"<style>{CSS}</style>\n"
362
+ f"<style>{PIANO_CSS}</style>\n"
363
+ f"<script>{ASSETS_JS}</script>\n"
364
+ f"<script>window.MM_AUDIO_BASE = {json.dumps(AUDIO_BASE_URL)};</script>\n"
365
+ f"<script>{PIANO_GLOBALS}</script>\n"
366
+ f"<script>{GAME_JS}</script>\n"
367
+ f"<script>{PIANO_JS}</script>\n"
368
+ f"<script>{BOOTSTRAP_JS}</script>\n"
369
+ )
370
+
371
+ INTRO = """
372
+ # 🍄 Quazim0t0's Thousand Token Wood Entry
373
+ A mini **Dark-Souls-style** duel where the boss is controlled by a **Modular Mind** — six tiny
374
+ specialist networks that communicate through a **shared latent** (RecursiveLink) and a coordinator
375
+ that picks each move. The brain was **trained by self-play reinforcement learning**, not scripted.
376
+ Watch the right-hand panel: every boss decision shows which specialists fired and how the modulators
377
+ steer the fight through the latent. **Click *Enter the Fog* and click the game once to focus, then play.**
378
+ """
379
+
380
+ # ---- placeholder repo link (replace REPO_URL with your real GitHub URL) ------
381
+ REPO_URL = "https://github.com/your-username/ModularMind" # TODO: replace with the real repo
382
+ REPO_MD = f"""
383
+ ### 📦 Get the full Modular Mind project
384
+ This Space runs a **tiny, specialist-scale** version of the architecture. The full,
385
+ production-scale **Modular Mind** — transformer specialists (MLA / MoE / Hyper-Connections),
386
+ the `RecursiveLink` latent bridge, context-doubling and the residual latent highway — lives on GitHub:
387
+
388
+ ### 👉 [**Download / view the Modular Mind repo on GitHub**]({REPO_URL})
389
+ *(placeholder link — swap `{REPO_URL}` for your actual repository)*
390
+ """
391
+
392
+ TECH_MD = r"""
393
+ ## 🧠 How the model works (technical breakdown)
394
+
395
+ The boss brain is a faithful, **specialist-scale** implementation of the Modular Mind
396
+ architecture — small enough (~**4,500 parameters**, pure-NumPy inference) to decide in
397
+ well under a millisecond on a free CPU, yet structurally identical to the big idea:
398
+ **many small domain specialists that communicate through one shared latent.**
399
+
400
+ ### The pieces
401
+ - **7 specialists** (tiny 2-layer MLPs). Five *own an action* and two *modulators* own none:
402
+ | Specialist | Owns | Role |
403
+ |---|---|---|
404
+ | **Aggressor** | `CLEAVE` | attack when in range |
405
+ | **Stalker** | `APPROACH` | close the distance |
406
+ | **Survivor** | `RETREAT` | reset spacing when it can't swing |
407
+ | **Baiter** | `IDLE` | wait / bait a whiff |
408
+ | **Defender** | `BLOCK` | guard the player's melee when it can't punish |
409
+ | **Punisher** | — *(modulator)* | detects "the player is open / recovering" |
410
+ | **Enrage** | — *(modulator)* | detects "we're low on HP → go berserk" |
411
+ - **`RecursiveLink`** — a ReGLU + residual block that merges the six latents into **one shared latent** (the "bridge").
412
+ - **Coordinator** — a linear read-out of the shared latent that nudges every action's score.
413
+
414
+ ### What every specialist is doing *at one moment* (a single decision tick)
415
+ A souls boss commits to one move at a time, so the brain only fires when the boss is free
416
+ (~2–4 times/second). In that one forward pass, **all six specialists run in parallel**:
417
+
418
+ 1. **Perceive** — the live game state is compressed to a **10-D feature vector** (distance, in-range?, boss HP, player HP, cooldown ready?, is the player attacking / recovering / blocking?).
419
+ 2. **Specialise** — each specialist computes `h = tanh(W₁·features)` and emits a **latent vector** `zᵢ` (its "opinion"); the four action-owners also emit a scalar **drive** for their move.
420
+ 3. **Communicate** — the six latents are summed and pushed through the **`RecursiveLink`** to form the **shared latent** `s`. This is the only channel the **modulators** have: *Punisher* writes "player is open" and *Enrage* writes "HP is low" into `s` — they cast no direct vote.
421
+ 4. **Coordinate** — the **coordinator** reads `s` and produces a **modulation** added to each action's score. So `score(action) = (owner's drive) + (coordinator modulation)`. This is where "the player is open" turns *Aggressor's* CLEAVE up, or "we're low HP" makes the boss commit harder.
422
+ 5. **Act** — the boss takes the top-scoring legal action (CLEAVE is masked while on cooldown). A small per-difficulty *mistake rate* adds the easy/normal/hard feel.
423
+
424
+ That whole loop is the **4-bar specialist panel + shared-latent strip** you see updating in the game — a live X-ray of the model thinking.
425
+
426
+ ### How it learned
427
+ Trained by **self-play REINFORCE** (policy gradient + value baseline) in a headless duel
428
+ simulator: reward = *damage dealt − damage taken*, plus shaping that rewards pressuring in
429
+ range and punishes stalling. Over ~700 batches the win-rate climbed against a near-optimal
430
+ dodging opponent and the tactics — spacing, punishing recovery frames, blocking your punish,
431
+ enraging at low HP — **emerged**; none of it is hand-scripted. The **difficulty tiers are the
432
+ same trained brain at different decision-noise levels** (Easy makes more exploitable mistakes,
433
+ Hard plays sharp ≈0.95 win vs the dodger).
434
+
435
+ ### Why the structure matters
436
+ - **Modular** — you can retrain or swap one specialist without touching the others (e.g. the **Defender/BLOCK** specialist was added later and the rest were untouched).
437
+ - **Explainable** — at any instant you can read *which* specialist drove the decision and how the modulators bent it.
438
+ - **Cheap** — specialists are small and run in parallel; the latent bridge is one tiny matmul.
439
+
440
+ ### It finetunes from *your* fights (online learning)
441
+ Because the model is tiny, a gradient step is microseconds — so the boss can learn
442
+ from real play **on this CPU**. Every HARD-tier fight is logged (state, action, HP per
443
+ decision) and sent to a `/learn` endpoint; we rebuild the per-decision rewards (damage
444
+ dealt − taken, + kill / − death), compute REINFORCE returns, and take **one Adam step**
445
+ that nudges the HARD brain toward what worked against real humans — the backprop is
446
+ hand-written in numpy and verified against PyTorch to ~1e-8. A frozen copy of the
447
+ sim-trained weights is an **anchor** (gentle pull-back) so it can't drift into nonsense,
448
+ and with a `HF_TOKEN` + `MM_DATASET_REPO` secret the adapted weights persist to a
449
+ HuggingFace Dataset across Space restarts. (Only HARD fights train, so the adaptation data
450
+ stays on-policy.)
451
+ """
452
+
453
+ USES_MD = r"""
454
+ ## 🌍 Three real-world applications of this architecture
455
+
456
+ The reusable idea isn't "a boss" — it's **small, independently-trainable specialists that
457
+ coordinate through a shared latent instead of through brittle hand-written rules or one giant
458
+ monolithic model.** That pattern transfers well beyond games:
459
+
460
+ **1. On-device / edge robotics & IoT control.**
461
+ A drone, robot arm, or wearable can't run a huge policy. Give it a handful of tiny specialists
462
+ — *balance*, *obstacle-avoidance*, *navigation*, *battery/thermal management* — each cheap
463
+ enough for a microcontroller, coordinating through one shared latent. You can **add or replace
464
+ a specialist** (e.g., a new sensor) without retraining the whole stack, and the latent bridge
465
+ fuses their context in a single cheap step — exactly what this boss does at 2–4 Hz on a CPU.
466
+
467
+ **2. Explainable, designer-tunable AI for games & simulations.**
468
+ Studios want NPC/boss/crowd AI that's *steerable and inspectable*, not a black box. With this
469
+ pattern a designer can tune or hot-swap one behavior specialist (more aggressive, more cautious)
470
+ and **see exactly which specialist fired** for any decision — the same live panel shown here.
471
+ That makes balancing, debugging, and difficulty tuning tractable in ways a single end-to-end
472
+ policy isn't.
473
+
474
+ **3. Modular AI agents / mixture-of-specialists that talk in latent space.**
475
+ The original Modular Mind motivation: instead of an "agent chain" that re-serializes everything
476
+ to **text** at every hop (lossy, slow), let domain specialists — *math*, *code*, *retrieval*,
477
+ *safety/policy* — communicate through a **latent bridge** (`RecursiveLink` + a residual highway
478
+ for deep chains). A small language model can consult a math or tool specialist **without
479
+ flattening to tokens**, each specialist is trained/upgraded independently, and the system stays
480
+ auditable. Useful for cost-sensitive assistants, industrial decisioning (risk + liquidity +
481
+ fraud specialists), or clinical triage (modular diagnostic experts) where you must know *why*.
482
+ """
483
+
484
+
485
+ PIANO_INTRO = """
486
+ ### 🎹 Bonus: a self-playing piano — same Modular Mind method, trained on a song
487
+ Under the boss fight, the *same architecture* (tiny specialists → `RecursiveLink` → a coordinator)
488
+ applied to **playing piano in chords**. It was trained by **multi-note next-frame prediction** on a
489
+ *polyphonic* transcription of a song: six specialists (Bass / Tenor / Soprano registers + Sustain /
490
+ Onset / Phrase modulators) emit latents, the bridge merges them, and the coordinator picks the **set
491
+ of notes** to play next. It plays itself with **real recorded acoustic-piano samples**. Press **play**
492
+ and watch the keys + specialists light up.
493
+ <sub>Rough by design — one song, a tiny model, crude polyphonic transcription — the *method carrying over* is the point.</sub>
494
+ """
495
+
496
+ with gr.Blocks(theme=gr.themes.Base(), title="Quazim0t0's 🍄 Thousand Token Wood Entry", head=HEAD) as demo:
497
+ gr.Markdown(INTRO)
498
+ gr.HTML(INDEX_HTML)
499
+
500
+ gr.Markdown(PIANO_INTRO)
501
+ gr.HTML(PIANO_HTML)
502
+
503
+ gr.Markdown(REPO_MD)
504
+ with gr.Accordion("🧠 How the Modular Mind works (technical breakdown)", open=False):
505
+ gr.Markdown(TECH_MD)
506
+ with gr.Accordion("🌍 Three real-world applications", open=False):
507
+ gr.Markdown(USES_MD)
508
+
509
+ # the third application, made real: a live mixture-of-experts at the bottom
510
+ if build_moe_panel is not None:
511
+ build_moe_panel()
512
+
513
+ # hidden API plumbing: the browser calls /decide via the Gradio REST API
514
+ inp = gr.Textbox(visible=False)
515
+ out = gr.Textbox(visible=False)
516
+ trigger = gr.Button(visible=False)
517
+ trigger.click(decide, inp, out, api_name="decide")
518
+
519
+ linp = gr.Textbox(visible=False)
520
+ lout = gr.Textbox(visible=False)
521
+ ltrigger = gr.Button(visible=False)
522
+ ltrigger.click(learn, linp, lout, api_name="learn")
523
+
524
+ pinp = gr.Textbox(visible=False)
525
+ pout = gr.Textbox(visible=False)
526
+ ptrigger = gr.Button(visible=False)
527
+ ptrigger.click(piano, pinp, pout, api_name="piano")
528
+
529
+
530
+ if __name__ == "__main__":
531
+ demo.queue(default_concurrency_limit=8).launch(
532
+ server_name="0.0.0.0",
533
+ server_port=int(os.environ.get("PORT", "7860")),
534
+ allowed_paths=[AUDIO_DIR, PIANO_SAMPLES_DIR],
535
+ )
assets_data.js ADDED
The diff for this file is too large to render. See raw diff
 
audio/sfx/hurt3_monster.wav ADDED
Binary file (57.6 kB). View file
 
audio/sfx/hurt_knight.wav ADDED
Binary file (20.1 kB). View file
 
audio/sfx/hurt_monster.wav ADDED
Binary file (99.4 kB). View file
 
audio/sfx/jump_knight.wav ADDED
Binary file (21.8 kB). View file
 
audio/sfx/roar2_monster.wav ADDED
Binary file (48.2 kB). View file
 
audio/sfx/roar3_monster.wav ADDED
Binary file (47.2 kB). View file
 
audio/sfx/roar4_monster.wav ADDED
Binary file (41.9 kB). View file
 
audio/sfx/roar5_monster.wav ADDED
Binary file (43.5 kB). View file
 
audio/sfx/roar6_monster.wav ADDED
Binary file (27.6 kB). View file
 
audio/sfx/roar_monster.wav ADDED
Binary file (40 kB). View file
 
audio/sfx/walk_boss.wav ADDED
Binary file (27.3 kB). View file
 
audio/sfx/walk_knight.wav ADDED
Binary file (22.9 kB). View file
 
features.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ features.py -- the single source of truth for what the Modular Mind sees and the
3
+ action set it chooses from. Imported by the duel simulator (training env), the
4
+ torch model, and the numpy inference brain so all three agree exactly.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+ ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"]
11
+
12
+ FEATURES = [
13
+ "abs_dist", # 0 normalised |player - boss|, 0 close .. 1 far
14
+ "in_cleave_range", # 1 player within the boss's cleave reach
15
+ "in_mid_range", # 2 player in the "step in and swing" band
16
+ "boss_hp", # 3 boss hp fraction
17
+ "player_hp", # 4 player hp fraction
18
+ "boss_ready", # 5 1 = cleave off cooldown, 0 = recovering
19
+ "player_pressuring", # 6 player attacking or moving in
20
+ "player_open", # 7 player in attack/roll recovery = punishable
21
+ "player_blocking", # 8 player is holding block
22
+ "player_threat", # 9 player is swinging AT the boss in melee range (block cue)
23
+ "bias", # 10
24
+ ]
25
+ NF = len(FEATURES)
26
+
27
+
28
+ def extract_features(s: dict) -> np.ndarray:
29
+ arena = float(s.get("arenaW", 900))
30
+ dist = abs(float(s["playerX"]) - float(s["bossX"]))
31
+ abs_dist = min(dist / (arena * 0.6), 1.0)
32
+ cleave_reach = float(s.get("cleaveReach", 160))
33
+ f = np.zeros(NF, dtype=np.float32)
34
+ f[0] = abs_dist
35
+ f[1] = 1.0 if dist <= cleave_reach else 0.0
36
+ f[2] = 1.0 if cleave_reach < dist <= cleave_reach * 2.2 else 0.0
37
+ f[3] = float(s.get("bossHP", 1.0))
38
+ f[4] = float(s.get("playerHP", 1.0))
39
+ f[5] = 1.0 if float(s.get("bossCooldown", 0.0)) <= 0.0 else 0.0
40
+ f[6] = 1.0 if (s.get("playerAttacking") or s.get("playerApproaching")) else 0.0
41
+ f[7] = 1.0 if s.get("playerRecovering") else 0.0
42
+ f[8] = 1.0 if s.get("playerBlocking") else 0.0
43
+ f[9] = 1.0 if s.get("playerThreat") else 0.0
44
+ f[10] = 1.0
45
+ return f
46
+
47
+
48
+ def legal_mask(s: dict) -> np.ndarray:
49
+ """CLEAVE is illegal while on cooldown; everything else always allowed."""
50
+ m = np.ones(len(ACTIONS), dtype=np.float32)
51
+ if float(s.get("bossCooldown", 0.0)) > 0.0:
52
+ m[ACTIONS.index("CLEAVE")] = 0.0
53
+ return m
mm_grad.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ mm_grad.py -- pure-numpy forward + backward (REINFORCE gradient) for the Modular
3
+ Mind policy, so the boss can be **finetuned from real player data on a CPU** with
4
+ no torch at runtime.
5
+
6
+ The math is identical to mm_torch.ModularMindPolicy, hand-differentiated so a
7
+ gradient step is a few thousand FLOPs (microseconds). Verified against torch
8
+ autograd in test_grad() to <1e-6.
9
+
10
+ Pipeline:
11
+ player plays a fight -> browser logs (state, action, bossHP, playerHP) per boss
12
+ decision + who died -> /learn -> we rebuild the per-step rewards (damage dealt
13
+ - taken, + kill/- death), compute REINFORCE returns, and take one Adam step that
14
+ nudges the policy toward what worked against real humans. A frozen copy of the
15
+ sim-trained weights is kept as an anchor (small pull-back) so it can't drift far.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ from features import ACTIONS, NF, extract_features, legal_mask
22
+ from modular_mind import SPEC_DEFS, D_LATENT, H
23
+
24
+ NA = len(ACTIONS)
25
+ EPS = 1e-5
26
+
27
+
28
+ def _ln_fwd(x, w, b):
29
+ mu = x.mean()
30
+ var = ((x - mu) ** 2).mean()
31
+ std = np.sqrt(var + EPS)
32
+ xhat = (x - mu) / std
33
+ return xhat * w + b, (xhat, std, w)
34
+
35
+
36
+ def _ln_bwd(gy, cache):
37
+ xhat, std, w = cache
38
+ n = xhat.shape[0]
39
+ gw = gy * xhat
40
+ gb = gy.copy()
41
+ gxhat = gy * w
42
+ gx = (gxhat - gxhat.mean() - xhat * (gxhat * xhat).mean()) / std
43
+ return gx, gw, gb
44
+
45
+
46
+ def _relu(x):
47
+ return np.maximum(x, 0.0)
48
+
49
+
50
+ class OnlineLearner:
51
+ """Holds the live weights + Adam state; updates them from player trajectories."""
52
+
53
+ def __init__(self, weights, lr=5e-3, gamma=0.97, anchor_pull=0.02,
54
+ w_deal=6.0, w_take=5.0, time_pen=0.01, entropy_coef=0.01):
55
+ self.W = {k: v.astype(np.float64).copy() for k, v in weights.items()}
56
+ self.anchor = {k: v.copy() for k, v in self.W.items()} # sim-trained anchor
57
+ self.lr, self.gamma, self.anchor_pull = lr, gamma, anchor_pull
58
+ self.w_deal, self.w_take, self.time_pen = w_deal, w_take, time_pen
59
+ self.entropy_coef = entropy_coef
60
+ self.owns = [ACTIONS.index(o) if o else None for _, o, _ in SPEC_DEFS]
61
+ self.m = {k: np.zeros_like(v) for k, v in self.W.items()}
62
+ self.v = {k: np.zeros_like(v) for k, v in self.W.items()}
63
+ self.t = 0
64
+
65
+ # ---- forward with cached intermediates -------------------------------
66
+ def _forward(self, f):
67
+ W = self.W
68
+ hs, lats, drives = [], [], np.zeros(NA)
69
+ for i, owns in enumerate(self.owns):
70
+ pre = W[f"s{i}_fc1_w"] @ f + W[f"s{i}_fc1_b"]
71
+ h = np.tanh(pre)
72
+ hs.append(h)
73
+ lat = W[f"s{i}_lat_w"] @ h + W[f"s{i}_lat_b"]
74
+ lats.append(lat)
75
+ if owns is not None:
76
+ drives[owns] += W[f"s{i}_drv_w"][0] @ h + W[f"s{i}_drv_b"][0]
77
+ z = np.sum(lats, axis=0)
78
+ zn, ln_in_c = _ln_fwd(z, W["link_ni_w"], W["link_ni_b"])
79
+ pre_g = W["link_g"] @ zn
80
+ g_act = _relu(pre_g)
81
+ v_act = W["link_v"] @ zn
82
+ reglu = g_act * v_act
83
+ out = W["link_d"] @ reglu
84
+ shared, ln_out_c = _ln_fwd(out + z, W["link_no_w"], W["link_no_b"])
85
+ modulation = W["coord_w"] @ shared + W["coord_b"]
86
+ logits = drives + modulation
87
+ cache = dict(f=f, hs=hs, lats=lats, z=z, zn=zn, ln_in_c=ln_in_c, pre_g=pre_g,
88
+ g_act=g_act, v_act=v_act, reglu=reglu, out=out, shared=shared,
89
+ ln_out_c=ln_out_c)
90
+ return logits, cache
91
+
92
+ # ---- backward: accumulate grads of (advantage * -logpi - H) ----------
93
+ def _backward(self, cache, g_logits, grads):
94
+ W = self.W
95
+ # coordinator
96
+ grads["coord_w"] += np.outer(g_logits, cache["shared"])
97
+ grads["coord_b"] += g_logits
98
+ g_shared = W["coord_w"].T @ g_logits
99
+ # owned-action drives
100
+ g_drive = {}
101
+ for i, owns in enumerate(self.owns):
102
+ if owns is not None:
103
+ g_drive[i] = g_logits[owns]
104
+ # out + z layernorm
105
+ g_outz, gw, gb = _ln_bwd(g_shared, cache["ln_out_c"])
106
+ grads["link_no_w"] += gw
107
+ grads["link_no_b"] += gb
108
+ g_out = g_outz
109
+ g_z = g_outz.copy()
110
+ # out = Wd @ reglu
111
+ grads["link_d"] += np.outer(g_out, cache["reglu"])
112
+ g_reglu = W["link_d"].T @ g_out
113
+ # reglu = relu(Wg@zn) * (Wv@zn)
114
+ g_g_act = g_reglu * cache["v_act"]
115
+ g_v_act = g_reglu * cache["g_act"]
116
+ g_pre_g = g_g_act * (cache["pre_g"] > 0)
117
+ grads["link_g"] += np.outer(g_pre_g, cache["zn"])
118
+ grads["link_v"] += np.outer(g_v_act, cache["zn"])
119
+ g_zn = W["link_g"].T @ g_pre_g + W["link_v"].T @ g_v_act
120
+ # zn = layernorm(z)
121
+ g_z_ln, gw, gb = _ln_bwd(g_zn, cache["ln_in_c"])
122
+ grads["link_ni_w"] += gw
123
+ grads["link_ni_b"] += gb
124
+ g_z += g_z_ln
125
+ # z = sum(lat_i) -> each specialist
126
+ for i, owns in enumerate(self.owns):
127
+ h = cache["hs"][i]
128
+ g_lat = g_z
129
+ grads[f"s{i}_lat_w"] += np.outer(g_lat, h)
130
+ grads[f"s{i}_lat_b"] += g_lat
131
+ g_h = W[f"s{i}_lat_w"].T @ g_lat
132
+ if owns is not None:
133
+ grads[f"s{i}_drv_w"][0] += g_drive[i] * h
134
+ grads[f"s{i}_drv_b"][0] += g_drive[i]
135
+ g_h = g_h + W[f"s{i}_drv_w"][0] * g_drive[i]
136
+ g_pre = g_h * (1.0 - h * h)
137
+ grads[f"s{i}_fc1_w"] += np.outer(g_pre, cache["f"])
138
+ grads[f"s{i}_fc1_b"] += g_pre
139
+
140
+ def logpi_grad(self, f, action, advantage, mask):
141
+ """Grad of advantage * -log pi(action|state) (+ entropy bonus), accumulated."""
142
+ logits, cache = self._forward(f)
143
+ masked = np.where(mask > 0.5, logits, -1e9)
144
+ p = np.exp(masked - masked.max())
145
+ p = p / p.sum()
146
+ onehot = np.zeros(NA)
147
+ onehot[action] = 1.0
148
+ # d(-adv*logpi)/dlogits = adv*(p - onehot); entropy bonus grad = ent_coef*(p*(logp+H_)...)
149
+ g_logits = advantage * (p - onehot)
150
+ # entropy regularizer (encourage exploration): d(-ent_coef*H)/dlogits
151
+ with np.errstate(divide="ignore"):
152
+ logp = np.where(p > 1e-12, np.log(p), 0.0)
153
+ ent_term = self.entropy_coef * p * (logp + (p * (-logp)).sum())
154
+ g_logits = g_logits + np.where(mask > 0.5, ent_term, 0.0)
155
+ grads = {k: np.zeros_like(v) for k, v in self.W.items()}
156
+ self._backward(cache, g_logits, grads)
157
+ return grads
158
+
159
+ def _trajectory_rewards(self, steps, result):
160
+ """Rebuild per-decision rewards from logged HP (damage dealt - taken)."""
161
+ n = len(steps)
162
+ rews = np.zeros(n)
163
+ for t in range(n):
164
+ nb = steps[t + 1]["bossHP"] if t + 1 < n else (0.0 if result.get("bossDied") else steps[t]["bossHP"])
165
+ npl = steps[t + 1]["playerHP"] if t + 1 < n else (0.0 if result.get("playerDied") else steps[t]["playerHP"])
166
+ dealt = max(0.0, steps[t]["playerHP"] - npl)
167
+ taken = max(0.0, steps[t]["bossHP"] - nb)
168
+ rews[t] = dealt * self.w_deal - taken * self.w_take - self.time_pen
169
+ if result.get("playerDied"):
170
+ rews[-1] += 8.0
171
+ elif result.get("bossDied"):
172
+ rews[-1] -= 5.0
173
+ return rews
174
+
175
+ def update(self, trajectories):
176
+ """trajectories: list of {steps:[{state,action,bossHP,playerHP}], result:{}}.
177
+ Returns dict of stats. Mutates self.W in place (one Adam step)."""
178
+ grads = {k: np.zeros_like(v) for k, v in self.W.items()}
179
+ all_returns, nsteps = [], 0
180
+ # first pass: gather returns for a baseline
181
+ per_traj = []
182
+ for tr in trajectories:
183
+ steps = tr.get("steps", [])
184
+ if len(steps) < 2:
185
+ continue
186
+ rews = self._trajectory_rewards(steps, tr.get("result", {}))
187
+ G = np.zeros(len(rews))
188
+ acc = 0.0
189
+ for t in reversed(range(len(rews))):
190
+ acc = rews[t] + self.gamma * acc
191
+ G[t] = acc
192
+ per_traj.append((steps, G))
193
+ all_returns.extend(G.tolist())
194
+ if not per_traj:
195
+ return {"updated": False, "reason": "not enough data"}
196
+ baseline = float(np.mean(all_returns))
197
+ adv_std = float(np.std(all_returns)) + 1e-6
198
+ # second pass: accumulate gradient
199
+ for steps, G in per_traj:
200
+ for t, st in enumerate(steps):
201
+ s = st["state"]
202
+ f = extract_features(s).astype(np.float64)
203
+ mask = legal_mask(s)
204
+ action = ACTIONS.index(st["action"]) if isinstance(st["action"], str) else int(st["action"])
205
+ adv = (G[t] - baseline) / adv_std
206
+ g = self.logpi_grad(f, action, adv, mask)
207
+ for k in grads:
208
+ grads[k] += g[k]
209
+ nsteps += 1
210
+ # average + anchor pull-back (stay near the sim-trained policy)
211
+ self.t += 1
212
+ b1, b2 = 0.9, 0.999
213
+ for k in self.W:
214
+ gk = grads[k] / max(1, nsteps) + self.anchor_pull * (self.W[k] - self.anchor[k])
215
+ self.m[k] = b1 * self.m[k] + (1 - b1) * gk
216
+ self.v[k] = b2 * self.v[k] + (1 - b2) * (gk * gk)
217
+ mhat = self.m[k] / (1 - b1 ** self.t)
218
+ vhat = self.v[k] / (1 - b2 ** self.t)
219
+ self.W[k] -= self.lr * mhat / (np.sqrt(vhat) + 1e-8)
220
+ return {"updated": True, "steps": nsteps, "trajectories": len(per_traj),
221
+ "avg_return": round(baseline, 3)}
222
+
223
+
224
+ def test_grad():
225
+ """Verify the numpy logpi-gradient matches torch autograd."""
226
+ import torch
227
+ from mm_torch import ModularMindPolicy
228
+ m = ModularMindPolicy().double()
229
+ m.export_npz("_gradchk.npz")
230
+ W = {k: v for k, v in np.load("_gradchk.npz").items()}
231
+ learner = OnlineLearner(W, entropy_coef=0.0)
232
+ rng = np.random.default_rng(0)
233
+ maxrel = 0.0
234
+ for _ in range(5):
235
+ f = rng.normal(size=NF)
236
+ action = int(rng.integers(NA))
237
+ mask = np.ones(NA)
238
+ # numpy grad of -logpi(action) (advantage=1)
239
+ gnp = learner.logpi_grad(f, action, 1.0, mask)
240
+ # torch grad
241
+ m.zero_grad()
242
+ x = torch.tensor(f, dtype=torch.float64).unsqueeze(0)
243
+ logits, _ = m(x)
244
+ logp = torch.log_softmax(logits, dim=-1)[0, action]
245
+ (-logp).backward()
246
+ # compare coordinator weight grad as a representative
247
+ gt = m.coordinator.weight.grad.numpy()
248
+ rel = np.abs(gnp["coord_w"] - gt).max() / (np.abs(gt).max() + 1e-9)
249
+ maxrel = max(maxrel, rel)
250
+ import os
251
+ os.remove("_gradchk.npz")
252
+ print(f"max relative grad error (coord_w) vs torch: {maxrel:.2e}")
253
+ return maxrel
254
+
255
+
256
+ if __name__ == "__main__":
257
+ test_grad()
modular_mind.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ modular_mind.py -- numpy inference for the trained Modular Mind boss brain.
3
+
4
+ Loads the weights produced by train.py (mm_weights.npz) and runs the exact same
5
+ forward pass as mm_torch.ModularMindPolicy, in pure numpy (no torch needed at game
6
+ runtime -> tiny, fast, instant cold-start on a HuggingFace Space).
7
+
8
+ decide(state) returns the chosen boss action plus rich telemetry (per-specialist
9
+ drives, the shared latent, the coordinator's modulation) so the game can VISUALISE
10
+ the Modular Mind making each decision -- including the two modulator specialists
11
+ (Punisher, Enrage) whose only influence is the latent they write into the bridge.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+
17
+ import numpy as np
18
+
19
+ from features import ACTIONS, NF, extract_features, legal_mask
20
+
21
+ # must match mm_torch.SPEC_DEFS
22
+ SPEC_DEFS = [
23
+ ("Aggressor", "CLEAVE", "#ff4d4d"),
24
+ ("Stalker", "APPROACH", "#4da6ff"),
25
+ ("Survivor", "RETREAT", "#9b59b6"),
26
+ ("Baiter", "IDLE", "#f1c40f"),
27
+ ("Defender", "BLOCK", "#48b0c4"),
28
+ ("Punisher", None, "#e67e22"),
29
+ ("Enrage", None, "#c0392b"),
30
+ ]
31
+ WEIGHTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mm_weights.npz")
32
+ H, D_LATENT = 24, 12
33
+
34
+
35
+ def _layernorm(x, w, b, eps=1e-5):
36
+ mu = x.mean()
37
+ var = ((x - mu) ** 2).mean()
38
+ return (x - mu) / np.sqrt(var + eps) * w + b
39
+
40
+
41
+ def _relu(x):
42
+ return np.maximum(x, 0.0)
43
+
44
+
45
+ class ModularMind:
46
+ def __init__(self, weights_path=WEIGHTS_PATH):
47
+ if os.path.exists(weights_path):
48
+ self.W = {k: v for k, v in np.load(weights_path).items()}
49
+ self.trained = True
50
+ else: # fallback so the game still runs before training finishes
51
+ self.W = self._random_weights()
52
+ self.trained = False
53
+
54
+ def _random_weights(self):
55
+ rng = np.random.default_rng(0)
56
+ W = {}
57
+ for i, (_, owns, _) in enumerate(SPEC_DEFS):
58
+ W[f"s{i}_fc1_w"] = rng.normal(0, .3, (H, NF))
59
+ W[f"s{i}_fc1_b"] = np.zeros(H)
60
+ W[f"s{i}_lat_w"] = rng.normal(0, .3, (D_LATENT, H))
61
+ W[f"s{i}_lat_b"] = np.zeros(D_LATENT)
62
+ if owns is not None:
63
+ W[f"s{i}_drv_w"] = rng.normal(0, .3, (1, H))
64
+ W[f"s{i}_drv_b"] = np.zeros(1)
65
+ W["link_ni_w"] = np.ones(D_LATENT); W["link_ni_b"] = np.zeros(D_LATENT)
66
+ W["link_v"] = rng.normal(0, .3, (2 * D_LATENT, D_LATENT))
67
+ W["link_g"] = rng.normal(0, .3, (2 * D_LATENT, D_LATENT))
68
+ W["link_d"] = rng.normal(0, .3, (D_LATENT, 2 * D_LATENT))
69
+ W["link_no_w"] = np.ones(D_LATENT); W["link_no_b"] = np.zeros(D_LATENT)
70
+ W["coord_w"] = rng.normal(0, .3, (len(ACTIONS), D_LATENT))
71
+ W["coord_b"] = np.zeros(len(ACTIONS))
72
+ return W
73
+
74
+ def decide(self, state: dict, explore: float = 0.06):
75
+ W = self.W
76
+ f = extract_features(state).astype(np.float64)
77
+
78
+ drives = np.zeros(len(ACTIONS))
79
+ latents = []
80
+ per_spec = []
81
+ for i, (name, owns, color) in enumerate(SPEC_DEFS):
82
+ h = np.tanh(W[f"s{i}_fc1_w"] @ f + W[f"s{i}_fc1_b"])
83
+ lat = W[f"s{i}_lat_w"] @ h + W[f"s{i}_lat_b"]
84
+ latents.append(lat)
85
+ drv = None
86
+ if owns is not None:
87
+ drv = float((W[f"s{i}_drv_w"] @ h + W[f"s{i}_drv_b"]).item())
88
+ drives[ACTIONS.index(owns)] += drv
89
+ per_spec.append({
90
+ "name": name, "owns": owns, "color": color,
91
+ "drive": round(drv, 3) if drv is not None else None,
92
+ "latent_norm": round(float(np.linalg.norm(lat)), 3),
93
+ })
94
+
95
+ # RecursiveLink: shared latent bridge
96
+ z = np.sum(latents, axis=0)
97
+ zn = _layernorm(z, W["link_ni_w"], W["link_ni_b"])
98
+ reglu = _relu(W["link_g"] @ zn) * (W["link_v"] @ zn)
99
+ out = W["link_d"] @ reglu
100
+ shared = _layernorm(out + z, W["link_no_w"], W["link_no_b"])
101
+
102
+ # Coordinator read-out (modulator influence flows in here)
103
+ modulation = W["coord_w"] @ shared + W["coord_b"]
104
+ logits = drives + modulation
105
+
106
+ mask = legal_mask(state)
107
+ masked = np.where(mask > 0.5, logits, -1e9)
108
+
109
+ probs = np.exp(masked - masked.max())
110
+ probs = probs / probs.sum()
111
+ # `explore` is a true mistake-rate: with prob `explore` take a uniformly
112
+ # random LEGAL action (the difficulty dial), else the best action. (Sampling
113
+ # from `probs` barely degrades play -- the policy is too confident -- so we
114
+ # use uniform mistakes to make easier tiers actually easier.)
115
+ if explore > 0 and np.random.random() < explore:
116
+ legal_idx = np.where(mask > 0.5)[0]
117
+ choice = int(np.random.choice(legal_idx))
118
+ else:
119
+ choice = int(np.argmax(masked))
120
+
121
+ phase = 2 if state.get("bossHP", 1.0) < 0.5 else 1
122
+ return {
123
+ "action": ACTIONS[choice],
124
+ "phase": phase,
125
+ "trained": self.trained,
126
+ "specialists": per_spec,
127
+ "base_drive": {a: round(float(d), 3) for a, d in zip(ACTIONS, drives)},
128
+ "modulation": {a: round(float(m), 3) for a, m in zip(ACTIONS, modulation)},
129
+ "final_drive": {a: round(float(d), 3) for a, d in zip(ACTIONS, logits)},
130
+ "probs": {a: round(float(p), 3) for a, p in zip(ACTIONS, probs)},
131
+ "legal": {a: bool(m > 0.5) for a, m in zip(ACTIONS, mask)},
132
+ "shared_latent": [round(float(x), 3) for x in shared],
133
+ }
134
+
135
+
136
+ # ---- difficulty tiers --------------------------------------------------------
137
+ # Each tier = a different training checkpoint PLUS a decision-noise level. Easy is a
138
+ # partially-trained brain that also "thinks loosely" (more exploration -> more
139
+ # mistakes, more openings for the player); Hard is the fully-trained brain playing
140
+ # sharply. Missing weight files fall back to the hard weights.
141
+ _DIR = os.path.dirname(os.path.abspath(__file__))
142
+ # Difficulty = decision-noise on the trained brain. `explore` is the mistake-rate
143
+ # (prob of a random legal action); higher = the boss makes more exploitable mistakes.
144
+ # (Once the boss learned to BLOCK it dominates the sim even early in training, so a
145
+ # "weaker checkpoint" no longer yields an easy boss -- noise is the controllable dial:
146
+ # vs a near-optimal dodger these give boss win-rates ~0.35 / ~0.65 / ~0.95.)
147
+ DIFFICULTY = {
148
+ "easy": {"file": "mm_weights.npz", "explore": 0.50},
149
+ "normal": {"file": "mm_weights.npz", "explore": 0.22},
150
+ "hard": {"file": "mm_weights.npz", "explore": 0.04},
151
+ }
152
+ _MINDS: dict[str, "ModularMind"] = {}
153
+
154
+
155
+ def get_mind(difficulty: str = "hard") -> ModularMind:
156
+ key = difficulty if difficulty in DIFFICULTY else "hard"
157
+ if key not in _MINDS:
158
+ path = os.path.join(_DIR, DIFFICULTY[key]["file"])
159
+ if not os.path.exists(path): # tier not trained yet
160
+ path = os.path.join(_DIR, DIFFICULTY["hard"]["file"])
161
+ mind = ModularMind(path)
162
+ # the HARD tier shares the online learner's live (player-adapted) weights,
163
+ # so finetuning from real fights takes effect immediately. (Lazy import to
164
+ # avoid an import cycle: online -> mm_grad -> modular_mind.)
165
+ if key == "hard":
166
+ try:
167
+ import online
168
+ if online.ENABLED:
169
+ mind.W = online.live_weights()
170
+ mind.trained = True
171
+ except Exception as e:
172
+ print(f"[modular_mind] online weights not shared ({e})")
173
+ _MINDS[key] = mind
174
+ return _MINDS[key]
175
+
176
+
177
+ def decide(state: dict) -> dict:
178
+ """Route a decision to the brain for the requested difficulty tier (each tier
179
+ has its own checkpoint and exploration/mistake level)."""
180
+ key = str(state.get("difficulty", "hard"))
181
+ if key not in DIFFICULTY:
182
+ key = "hard"
183
+ out = get_mind(key).decide(state, explore=DIFFICULTY[key]["explore"])
184
+ out["difficulty"] = key
185
+ return out
online.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ online.py -- persistent finetuning of the boss from real player fights.
3
+
4
+ Flow: the browser logs every boss decision (state, action, HP) during a fight and,
5
+ on fight end, POSTs the trajectory + who-died to /learn. We buffer trajectories and,
6
+ every MM_UPDATE_EVERY fights, run one REINFORCE step (mm_grad.OnlineLearner) that
7
+ nudges the HARD brain toward what actually worked against humans. The adapted
8
+ weights feed straight back into the live boss.
9
+
10
+ ONLY HARD-tier fights are used, so the data stays on-policy (Easy/Normal are
11
+ deliberately handicapped checkpoints; learning from them would be off-policy).
12
+
13
+ Persistence (optional, set as Space secrets):
14
+ HF_TOKEN - a write token
15
+ MM_DATASET_REPO - e.g. "your-name/boss-fight-online"
16
+ If set, adapted weights are pushed to / pulled from that dataset so learning
17
+ survives Space restarts. Without them it still adapts live, just in-memory.
18
+
19
+ Safety: a frozen copy of the sim-trained weights is kept as an anchor (the learner
20
+ pulls gently back toward it), so a weird run can't brick the boss.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import threading
26
+
27
+ import numpy as np
28
+
29
+ from mm_grad import OnlineLearner
30
+
31
+ HERE = os.path.dirname(os.path.abspath(__file__))
32
+ BASE_WEIGHTS = os.path.join(HERE, "mm_weights.npz") # sim-trained HARD brain
33
+ LIVE_WEIGHTS = os.path.join(HERE, "mm_weights_live.npz") # player-adapted snapshot
34
+
35
+ ENABLED = os.environ.get("MM_ONLINE", "1") == "1"
36
+ UPDATE_EVERY = int(os.environ.get("MM_UPDATE_EVERY", "3")) # fights per update
37
+ ADAPT_TIER = "hard"
38
+ DATASET_REPO = os.environ.get("MM_DATASET_REPO")
39
+ HF_TOKEN = os.environ.get("HF_TOKEN")
40
+ WEIGHTS_IN_REPO = "mm_weights_live.npz"
41
+
42
+ _LOCK = threading.Lock()
43
+ _LEARNER = None
44
+ _BUFFER = []
45
+ _FIGHTS = 0
46
+
47
+
48
+ def _pull_from_dataset():
49
+ """Try to download the latest adapted weights from the HF dataset."""
50
+ if not (DATASET_REPO and HF_TOKEN):
51
+ return None
52
+ try:
53
+ from huggingface_hub import hf_hub_download
54
+ p = hf_hub_download(repo_id=DATASET_REPO, filename=WEIGHTS_IN_REPO,
55
+ repo_type="dataset", token=HF_TOKEN)
56
+ return {k: v for k, v in np.load(p).items()}
57
+ except Exception as e:
58
+ print(f"[online] no persisted weights pulled ({e}); starting from sim weights")
59
+ return None
60
+
61
+
62
+ def _push_to_dataset():
63
+ """Upload the current adapted weights to the HF dataset (best-effort)."""
64
+ if not (DATASET_REPO and HF_TOKEN):
65
+ return
66
+ try:
67
+ from huggingface_hub import upload_file
68
+ upload_file(path_or_fileobj=LIVE_WEIGHTS, path_in_repo=WEIGHTS_IN_REPO,
69
+ repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN)
70
+ except Exception as e:
71
+ print(f"[online] dataset push failed ({e})")
72
+
73
+
74
+ def get_learner():
75
+ global _LEARNER
76
+ if _LEARNER is None:
77
+ base = _pull_from_dataset()
78
+ if base is None and os.path.exists(LIVE_WEIGHTS):
79
+ base = {k: v for k, v in np.load(LIVE_WEIGHTS).items()}
80
+ if base is None:
81
+ base = {k: v for k, v in np.load(BASE_WEIGHTS).items()}
82
+ # anchor is ALWAYS the pristine sim-trained weights
83
+ _LEARNER = OnlineLearner(base)
84
+ _LEARNER.anchor = {k: v.astype(np.float64).copy()
85
+ for k, v in np.load(BASE_WEIGHTS).items()}
86
+ return _LEARNER
87
+
88
+
89
+ def live_weights():
90
+ """The HARD brain's current (possibly player-adapted) weights, shared live."""
91
+ return get_learner().W
92
+
93
+
94
+ def _save_live():
95
+ np.savez(LIVE_WEIGHTS, **{k: v.astype(np.float32) for k, v in get_learner().W.items()})
96
+ _push_to_dataset()
97
+
98
+
99
+ def record_fight(trajectory: dict) -> dict:
100
+ """Called by /learn. trajectory = {difficulty, steps:[{state,action,bossHP,playerHP}],
101
+ result:{bossDied,playerDied}}. Buffers it and updates every UPDATE_EVERY fights."""
102
+ global _FIGHTS
103
+ if not ENABLED:
104
+ return {"enabled": False}
105
+ if trajectory.get("difficulty") != ADAPT_TIER:
106
+ return {"enabled": True, "skipped": "only HARD-tier fights train the brain"}
107
+ if len(trajectory.get("steps", [])) < 2:
108
+ return {"enabled": True, "skipped": "too short"}
109
+ with _LOCK:
110
+ _BUFFER.append(trajectory)
111
+ _FIGHTS += 1
112
+ if len(_BUFFER) >= UPDATE_EVERY:
113
+ stats = get_learner().update(list(_BUFFER))
114
+ _BUFFER.clear()
115
+ if stats.get("updated"):
116
+ _save_live()
117
+ return {"enabled": True, "fights": _FIGHTS, **stats}
118
+ return {"enabled": True, "fights": _FIGHTS, "buffered": len(_BUFFER),
119
+ "update_in": UPDATE_EVERY - len(_BUFFER)}
120
+
121
+
122
+ def status() -> dict:
123
+ return {"enabled": ENABLED, "fights_seen": _FIGHTS, "buffered": len(_BUFFER),
124
+ "persistent": bool(DATASET_REPO and HF_TOKEN), "adapt_tier": ADAPT_TIER}
piano/notes.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"seq": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 23, 0, 23, 17, 11, 11, 11, 11, 17, 17, 17, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 13, 13, 13, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, 6, 9, 9, 12, 9, 9, 9, 13, 13, 13, 9, 13, 13, 13, 13, 13, 18, 23, 23, 23, 18, 13, 13, 4, 4, 4, 2, 2, 2, 2, 2, 2, 3, 4, 4, 4, 4, 4, 3, 2, 2, 2, 2, 3, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 20, 20, 20, 20, 20, 20, 16, 16, 16, 10, 9, 7, 6, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 8, 8, 8, 8, 8, 8, 8, 8, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 20, 20, 20, 20, 20, 14, 14, 14, 14, 14, 14, 14, 14, 20, 20, 22, 23, 23, 23, 23, 23, 21, 21, 21, 21, 20, 20, 20, 9, 2, 2, 2, 2, 20, 20, 20, 20, 21, 21, 21, 21, 21, 17, 17, 17, 17, 17, 8, 8, 8, 8, 8, 8, 5, 5, 1, 1, 1, 1, 1, 1, 3, 3, 5, 5, 1, 0, 0, 0, 0, 0, 0, 11, 15, 13, 13, 15, 13, 13, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 18, 18, 18, 18, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 11, 21, 21, 23, 23, 23, 23, 23, 14, 14, 14, 14, 14, 14, 14, 14, 18, 18, 18, 20, 20, 21, 21, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 21, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 10, 9, 9, 9, 10, 11, 20, 20, 20, 23, 23, 24, 25, 25, 25, 25, 13, 13, 13, 13, 23, 23, 23, 23, 23, 22, 21, 21, 21, 21, 21, 21, 21, 18, 13, 6, 6, 6, 6, 16, 16, 16, 16, 16, 16, 14, 14, 14, 13, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 24, 24, 24, 24, 13, 1, 1, 1, 1, 1, 1, 1, 3, 0, 5, 5, 11, 12, 13, 13, 13, 13, 15, 25, 0, 0, 0, 13, 0, 0, 15, 18, 18, 18, 18, 18, 18, 18, 18, 16, 16, 16, 16, 16, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 16, 13, 13, 13, 13, 12, 12, 8, 8, 8, 8, 9, 9, 9, 10, 20, 20, 20, 20, 21, 21, 21, 21, 18, 18, 18, 18, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 16, 16, 14, 14, 14, 13, 13, 12, 11, 11, 11, 11, 11, 18, 18, 18, 18, 18, 13, 13, 13, 13, 13, 14, 18, 18, 18, 18, 18, 20, 20, 20, 20, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 17, 16, 16, 16, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 16, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 19, 19, 18, 18, 18, 18, 18, 19, 20, 20, 20, 19, 18, 18, 18, 10, 9, 9, 9, 10, 21, 21, 21, 21, 9, 9, 9, 9, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 23, 23, 12, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 16, 18, 18, 21, 21, 21, 21, 21, 21, 19, 19, 18, 18, 18, 16, 16, 16, 16, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 11, 11, 11, 11, 13, 13, 13, 13, 21, 23, 23, 23, 23, 23, 21, 18, 12, 12, 11, 11, 11, 11, 11, 11, 11, 12, 13, 13, 18, 20, 20, 20, 21, 21, 20, 20, 20, 16, 16, 16, 16, 18, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 20, 20, 20, 20, 9, 9, 9, 9, 9, 15, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 20, 20, 20, 20, 23, 23, 23, 23, 21, 21, 21, 21, 21, 14, 14, 14, 14, 14, 14, 18, 25, 25, 25, 25, 25, 25, 25, 25, 24, 24, 24, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 18, 13, 18, 13, 18, 18, 0, 0, 0, 0, 0, 13, 13, 13, 13, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 25, 25, 23, 23, 23, 13, 21, 23, 21, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 13, 25, 25, 25, 25, 25, 23, 23, 22, 21, 21, 17, 17, 17, 17, 18, 18, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 14, 9, 9, 8, 9, 9, 9, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 13, 13, 9, 9, 9, 14, 25, 25, 25, 25, 25, 25, 25, 25, 20, 19, 14, 14, 14, 14, 14, 14, 14, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 23, 25, 25, 25, 25, 25, 23, 5, 5, 5, 5, 5, 9, 13, 0, 13, 7, 1, 1, 1, 1, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 8, 0, 13, 0, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 14, 23, 23, 23, 23, 23, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 18, 18, 17, 17, 17, 17, 17, 18, 18, 18, 18, 7, 7, 6, 6, 6, 6, 7, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 11, 11, 11, 11, 11, 10, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 13, 13, 13, 13, 13, 13, 13, 23, 23, 23, 23, 23, 13, 13, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 18, 21, 21, 21, 21, 25, 25, 25, 25, 21, 10, 9, 9, 9, 9, 20, 20, 20, 20, 20, 20, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 11, 11, 13, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 11, 11, 11, 11, 11, 11, 11, 9, 9, 6, 6, 6, 6, 6, 9, 9, 9, 17, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 17, 13, 9, 6, 6, 0, 0, 0, 0, 0, 16, 16, 0, 14, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 11, 13, 13, 13, 9, 6, 6, 6, 6, 6, 7, 9, 9, 9, 9, 11, 11, 11, 11, 11, 13, 13, 16, 20, 20, 20, 20, 18, 18, 18, 17, 17, 18, 18, 18, 18, 13, 15, 15, 13, 13, 13, 7, 10, 20, 20, 20, 20, 20, 20, 20, 20, 20, 16, 13, 13, 13, 13, 13, 13, 14, 15, 14, 13, 13, 13, 13, 13, 13, 13, 11, 9, 9, 9, 9, 16, 22, 22, 22, 21, 21, 20, 16, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 17, 17, 18, 20, 20, 20, 20, 20, 20, 21, 22, 0, 22, 0, 22, 22, 22, 22, 22, 22, 20, 18, 13, 13, 13, 13, 15, 18, 20, 20, 0, 20, 20, 21, 22, 22, 22, 22, 0, 0, 0, 0, 20, 0, 0, 22, 22, 22, 22, 0, 0, 0, 0, 0, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 16, 20, 20, 17, 17, 17, 17, 18, 22, 22, 22, 22, 18, 15, 15, 15, 15, 15, 15, 15, 15, 19, 19, 20, 25, 25, 25, 25, 25, 15, 15, 15, 15, 15, 21, 27, 0, 27, 27, 25, 25, 25, 25, 25, 25, 17, 8, 10, 8, 8, 10, 0, 19, 27, 0, 27, 27, 25, 25, 25, 25, 25, 25, 25, 22, 20, 20, 20, 20, 20, 20, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 13, 13, 13, 13, 13, 13, 13, 10, 10, 10, 10, 10, 15, 15, 15, 15, 15, 15, 15, 13, 11, 10, 8, 8, 8, 8, 10, 19, 20, 20, 20, 20, 20, 20, 20, 20, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 19, 19, 11, 1, 1, 1, 1, 1, 8, 8, 9, 20, 20, 22, 22, 22, 20, 20, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 8, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 23, 23, 23, 23, 17, 16, 11, 10, 10, 10, 10, 10, 13, 13, 13, 13, 13, 15, 23, 23, 23, 23, 23, 23, 15, 13, 13, 11, 11, 11, 11, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 22, 22, 22, 22, 21, 21, 21, 21, 22, 22, 23, 23, 23, 23, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 22, 22, 20, 20, 20, 18, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 15, 15, 15, 15, 15, 15, 10, 12, 15, 15, 15, 15, 15, 15, 8, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 15, 15, 15, 15, 15, 22, 22, 22, 22, 22, 22, 22, 22, 16, 16, 11, 15, 15, 15, 19, 19, 20, 20, 0, 0, 0, 0, 0, 0, 20, 20, 16, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 18, 18, 19, 19, 19, 18, 0, 0, 0, 0, 3, 0, 0, 10, 12, 13, 12, 11, 11, 15, 15, 22, 22, 22, 22, 22, 16, 11, 11, 8, 6, 6, 6, 6, 6, 6, 6, 6, 10, 11, 11, 11, 11, 11, 11, 11, 15, 15, 15, 15, 15, 15, 15, 22, 22, 22, 23, 23, 23, 23, 22, 16, 16, 16, 16, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 4, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 20, 20, 20, 20, 20, 20, 13, 13, 13, 13, 13, 14, 14, 14, 16, 16, 15, 15, 15, 15, 16, 16, 23, 23, 23, 23, 23, 16, 16, 16, 15, 11, 10, 10, 10, 10, 11, 18, 22, 23, 23, 23, 22, 20, 13, 8, 8, 8, 8, 8, 8, 8, 10, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 10, 10, 10, 10, 16, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 19, 19, 19, 19, 19, 19, 11, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 5, 9, 9, 9, 9, 9, 9, 9, 14, 20, 20, 20, 20, 6, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 11, 12, 13, 13, 13, 13, 13, 13, 9, 9, 9, 9, 9, 9, 9, 9, 10, 11, 11, 11, 11, 12, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 11, 11, 11, 11, 11, 14, 17, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 18, 25, 25, 25, 25, 25, 25, 25, 23, 23, 20, 11, 11, 11, 11, 11, 11, 11, 11, 13, 18, 22, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 14, 18, 18, 18, 18, 18, 19, 20, 19, 15, 15, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 18, 18, 18, 18, 18, 13, 13, 13, 13, 13, 13, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 13, 13, 13, 13, 13, 13, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 9, 9, 9, 1, 4, 4, 4, 4, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 13, 13, 13, 13, 13, 13, 17, 17, 17, 17, 17, 17, 17, 17, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 12, 12, 19, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 23, 25, 25, 25, 25, 21, 21, 21, 21, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 16, 16, 16, 16, 16, 11, 11, 11, 6, 6, 6, 6, 6, 9, 11, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 17, 13, 13, 13, 13, 13, 13, 13, 13, 1, 1, 1, 1, 1, 2, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 20, 20, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 12, 11, 11, 11, 11, 7, 4, 4, 4, 10, 11, 12, 13, 13, 13, 13, 13, 13, 13, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 11, 11, 14, 19, 20, 21, 21, 20, 21, 21, 20, 20, 20, 20, 20, 18, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16, 25, 25, 25, 25, 25, 25, 25, 25, 25, 22, 10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 0, 0, 23, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 15, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 6, 6, 6, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 6, 6, 6, 6, 5, 5, 5, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 10, 11, 11, 12, 13, 13, 15, 18, 20, 20, 20, 21, 21, 21, 21, 0, 6, 0, 0, 0, 0, 21, 0, 6, 6, 6, 6, 6, 25, 25, 25, 25, 21, 9, 9, 9, 6, 6, 6, 6, 6, 6, 7, 7, 6, 4, 5, 8, 14, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 25, 25, 25, 25, 25, 25, 25, 25, 12, 11, 11, 11, 11, 11, 11, 11, 11, 17, 23, 23, 24, 24, 23, 23, 14, 14, 14, 14, 14, 14, 11, 11, 11, 11, 11, 11, 0, 0, 0, 0, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 20, 22, 22, 22, 23, 23, 23, 20, 20, 20, 20, 19, 20, 20, 20, 20, 20, 14, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 1, 1, 1, 9, 1, 1, 6, 9, 6, 3, 3, 7, 9, 9, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 21, 21, 21, 19, 18, 18, 18, 18, 18, 18, 18, 6, 6, 6, 6, 6, 13, 15, 21, 21, 21, 15, 9, 5, 0, 0, 0, 0, 25, 0, 0, 6, 6, 6, 6, 7, 9, 13, 21, 21, 21, 21, 21, 21, 23, 23, 24, 25, 25, 25, 25, 25, 25, 9, 5, 6, 15, 15, 15, 15, 19, 21, 21, 18, 18, 18, 18, 21, 21, 21, 21, 21, 19, 19, 18, 18, 19, 21, 21, 21, 21, 23, 22, 21, 19, 19, 20, 20, 20, 20, 22, 22, 20, 21, 21, 19, 19, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 11, 11, 11, 11, 19, 23, 25, 26, 25, 25, 25, 25, 23, 23, 23, 24, 23, 23, 23, 23, 23, 23, 20, 20, 23, 23, 25, 25, 25, 25, 25, 25, 25, 25, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 20, 20, 20, 13, 13, 13, 13, 13, 13, 13, 13, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 23, 23, 24, 25, 25, 25, 25, 25, 25, 25, 18, 18, 18, 18, 18, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 21, 21, 18, 18, 18, 18, 16, 16, 16, 16, 16, 16, 17, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 18, 18, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 21, 22, 21, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 24, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25, 25, 18, 13, 14, 14, 13, 13, 13, 13, 12, 11, 11, 11, 11, 11, 11, 8, 6, 6, 6, 14, 23, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 6, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 15, 15, 15, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 18, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 22, 22, 21, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 25, 25, 25, 25, 25, 23, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 21, 21, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 16, 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 13, 10, 10, 10, 10, 11, 11, 11, 20, 20, 20, 20, 20, 20, 20, 15, 9, 9, 9, 9, 9, 9, 9, 9, 9, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 14, 13, 13, 13, 13, 15, 17, 17, 17, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 23, 23, 23, 21, 21, 21, 21, 21, 21, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 5, 5, 5, 5, 5, 5, 3, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 7, 13, 13, 13, 16, 0, 20, 19, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 11, 9, 9, 8, 2, 2, 2, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 13, 13, 15, 15, 15, 15, 15, 14, 14, 13, 13, 13, 8, 8, 8, 8, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 0, 0, 0, 0, 16, 23, 23, 23, 23, 23, 10, 10, 9, 9, 10, 10, 11, 20, 20, 20, 20, 23, 23, 26, 26, 26, 23, 13, 13, 13, 13, 13, 13, 12, 12, 12, 13, 17, 21, 24, 24, 24, 24, 21, 18, 13, 13, 13, 13, 15, 16, 16, 16, 16, 16, 16, 16, 14, 14, 13, 13, 13, 13, 5, 5, 5, 13, 25, 25, 25, 25, 25, 25, 25, 18, 8, 5, 5, 5, 5, 5, 5, 5, 5, 11, 17, 17, 17, 0, 0, 0, 0, 0, 11, 11, 0, 23, 0, 0, 0, 0, 18, 18, 18, 18, 18, 18, 18, 18, 17, 16, 16, 16, 16, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 16, 16, 13, 13, 13, 13, 12, 8, 8, 8, 8, 8, 9, 9, 10, 20, 20, 20, 20, 21, 21, 21, 21, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 15, 14, 14, 14, 14, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 13, 13, 13, 10, 9, 9, 9, 9, 9, 18, 18, 18, 20, 22, 22, 22, 22, 20, 20, 20, 20, 19, 19, 19, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 25, 25, 25, 25, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 9, 9, 9, 9, 9, 18, 18, 18, 18, 18, 16, 16, 16, 16, 16, 16, 16, 16, 20, 20, 20, 20, 20, 20, 12, 11, 11, 10, 10, 10, 10, 13, 13, 13, 13, 13, 13, 13, 13, 13, 18, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 23, 23, 21, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 13, 13, 13, 13, 20, 20, 21, 21, 21, 21, 20, 20, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 6, 6, 6, 6, 6, 6, 6, 8, 21, 23, 23, 21, 18, 18, 18, 18, 18, 11, 11, 10, 10, 10, 9, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 14, 14, 14, 14, 14, 14, 14, 14, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 23, 23, 23, 17, 11, 11, 8, 5, 0, 0, 5, 0, 0, 13, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 20, 13, 13, 13, 13, 11, 11, 11, 13, 21, 21, 21, 21, 21, 21, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 13, 8, 8, 8, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 15, 9, 9, 9, 9, 9, 9, 9, 18, 18, 20, 20, 20, 20, 20, 20, 11, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 13, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 20, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 13, 13, 13, 13, 11, 20, 17, 0, 25, 25, 25, 13, 5, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 18, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 8, 8, 11, 11, 11, 12, 11, 11, 11, 9, 8, 7, 8, 8, 8, 8, 8, 8, 13, 13, 13, 13, 13, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 9, 8, 8, 8, 9, 13, 17, 17, 17, 17, 10, 10, 9, 10, 10, 11, 11, 11, 11, 11, 12, 12, 13, 11, 11, 9, 8, 8, 4, 4, 0, 0, 0, 0, 0, 0, 0, 17, 20, 20, 20, 14, 14, 14, 8, 2, 2, 2, 5, 8, 8, 8, 8, 8, 8, 8, 8, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tok2midi": {"0": 0, "1": 59, "2": 60, "3": 61, "4": 62, "5": 63, "6": 64, "7": 65, "8": 66, "9": 67, "10": 68, "11": 69, "12": 70, "13": 71, "14": 72, "15": 73, "16": 74, "17": 75, "18": 76, "19": 77, "20": 78, "21": 79, "22": 80, "23": 81, "24": 82, "25": 83, "26": 84, "27": 85}, "n_tokens": 28, "fps": 8, "midi_lo": 59, "midi_hi": 85}
piano/piano_mind.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ piano_mind.py -- a Modular Mind that plays piano, built with the SAME method as the
3
+ boss-fight brain: several tiny specialist MLPs emit latents, a RecursiveLink merges
4
+ them into one shared latent, and a coordinator reads it to pick the next note. Some
5
+ specialists "own" a register (Bass/Tenor/Soprano/Rest) and add a drive to those keys;
6
+ two are modulators (Onset/Phrase) that only write into the shared latent.
7
+
8
+ Trained by next-note prediction on a note sequence transcribed from a song
9
+ (piano/notes.json). Tiny -> trains in seconds and runs instantly for the self-playing
10
+ piano in the Space. torch is used (already a Space dep); the architecture mirrors
11
+ mm_torch / modular_mind.
12
+ """
13
+ from __future__ import annotations
14
+ import json, os
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ K = 40 # context window (frames of recent notes) = 5s at 8fps
21
+ H, D_LATENT = 64, 32
22
+ # (name, owned register or None) -- mirrors the boss's 5 owners + 2 modulators
23
+ SPECS = [("Bass", "low"), ("Tenor", "mid"), ("Soprano", "high"),
24
+ ("Rest", "rest"), ("Onset", None), ("Phrase", None)]
25
+
26
+
27
+ def _group_masks(n_tokens):
28
+ """token 0 = rest; notes 1..n-1 split into low/mid/high thirds."""
29
+ m = {g: torch.zeros(n_tokens) for g in ("low", "mid", "high", "rest")}
30
+ m["rest"][0] = 1.0
31
+ notes = list(range(1, n_tokens))
32
+ third = max(1, len(notes) // 3)
33
+ for j, t in enumerate(notes):
34
+ g = "low" if j < third else ("mid" if j < 2 * third else "high")
35
+ m[g][t] = 1.0
36
+ return m
37
+
38
+
39
+ class PianoMind(nn.Module):
40
+ def __init__(self, n_tokens):
41
+ super().__init__()
42
+ self.n_tokens = n_tokens
43
+ self.fc1 = nn.ModuleList([nn.Linear(K, H) for _ in SPECS])
44
+ self.lat = nn.ModuleList([nn.Linear(H, D_LATENT) for _ in SPECS])
45
+ self.drv = nn.ModuleDict({n: nn.Linear(H, 1) for n, owns in SPECS if owns})
46
+ # RecursiveLink (ReGLU + residual), same shape as the boss bridge
47
+ self.ni = nn.LayerNorm(D_LATENT)
48
+ self.v = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
49
+ self.g = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
50
+ self.d = nn.Linear(2 * D_LATENT, D_LATENT, bias=False)
51
+ self.no = nn.LayerNorm(D_LATENT)
52
+ self.coord = nn.Linear(D_LATENT, n_tokens)
53
+ gm = _group_masks(n_tokens)
54
+ for g, t in gm.items():
55
+ self.register_buffer(f"mask_{g}", t)
56
+
57
+ def forward(self, feat): # feat [B, K] in [0,1]
58
+ B = feat.shape[0]
59
+ drives = torch.zeros(B, self.n_tokens, device=feat.device)
60
+ lats = []
61
+ for i, (name, owns) in enumerate(SPECS):
62
+ h = torch.tanh(self.fc1[i](feat))
63
+ lats.append(self.lat[i](h))
64
+ if owns:
65
+ drives = drives + self.drv[name](h) * getattr(self, f"mask_{owns}")
66
+ z = torch.stack(lats, 0).sum(0)
67
+ zn = self.ni(z)
68
+ shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
69
+ return drives + self.coord(shared) # logits [B, n_tokens]
70
+
71
+ @torch.no_grad()
72
+ def telemetry(self, feat): # feat [1, K] -> logits, per-specialist info, shared
73
+ drives = torch.zeros(1, self.n_tokens)
74
+ lats, per = [], []
75
+ for i, (name, owns) in enumerate(SPECS):
76
+ hh = torch.tanh(self.fc1[i](feat))
77
+ lat = self.lat[i](hh); lats.append(lat)
78
+ d = None
79
+ if owns:
80
+ d = float(self.drv[name](hh).item())
81
+ drives = drives + d * getattr(self, f"mask_{owns}")
82
+ per.append({"name": name, "owns": owns,
83
+ "drive": round(d, 3) if d is not None else None,
84
+ "act": round(float(lat.norm().item()), 3)})
85
+ z = torch.stack(lats, 0).sum(0); zn = self.ni(z)
86
+ shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
87
+ logits = drives + self.coord(shared)
88
+ return logits[0], per, [round(float(v), 2) for v in shared[0].tolist()]
89
+
90
+
91
+ def _seq_feats(seq, n_tokens):
92
+ import torch
93
+ s = torch.tensor(seq, dtype=torch.long)
94
+ X, Y = [], []
95
+ for t in range(K, len(s)):
96
+ X.append(s[t - K:t].float() / n_tokens)
97
+ Y.append(s[t])
98
+ return torch.stack(X), torch.tensor(Y)
99
+
100
+
101
+ def train_and_save(notes_path=os.path.join(HERE, "notes.json"),
102
+ out=os.path.join(HERE, "piano_weights.pt"), epochs=900, seed=0):
103
+ torch.manual_seed(seed)
104
+ meta = json.load(open(notes_path))
105
+ seq, n_tokens = meta["seq"], meta["n_tokens"]
106
+ X, Y = _seq_feats(seq, n_tokens)
107
+ model = PianoMind(n_tokens)
108
+ opt = torch.optim.Adam(model.parameters(), lr=1e-2)
109
+ n = X.shape[0]; bs = 512
110
+ for ep in range(epochs):
111
+ perm = torch.randperm(n)
112
+ tot = 0.0
113
+ for i in range(0, n, bs):
114
+ idx = perm[i:i + bs]
115
+ logits = model(X[idx])
116
+ loss = F.cross_entropy(logits, Y[idx])
117
+ opt.zero_grad(); loss.backward(); opt.step()
118
+ tot += loss.item() * len(idx)
119
+ if ep % 80 == 0 or ep == epochs - 1:
120
+ with torch.no_grad():
121
+ acc = (model(X).argmax(1) == Y).float().mean().item()
122
+ print(f" epoch {ep:4d} loss {tot/n:.3f} next-note acc {acc:.3f}")
123
+ import numpy as _np
124
+ arr = _np.array(seq) # seed from the most-sounding window (song fades in)
125
+ bi = int(max(range(len(seq) - K), key=lambda i: int((arr[i:i + K] > 0).sum())))
126
+ torch.save({"state": model.state_dict(), "n_tokens": n_tokens,
127
+ "tok2midi": meta["tok2midi"], "K": K, "fps": meta.get("fps", 8),
128
+ "seed_seq": seq[bi:bi + K]}, out)
129
+ print(f"saved -> {out}")
130
+ return model, meta
131
+
132
+
133
+ class PianoPlayer:
134
+ """Loads the trained PianoMind and autoregressively yields the next note token."""
135
+ def __init__(self, weights=os.path.join(HERE, "piano_weights.pt")):
136
+ ck = torch.load(weights, map_location="cpu")
137
+ self.n_tokens = ck["n_tokens"]; self.K = ck["K"]; self.fps = ck["fps"]
138
+ self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
139
+ self.seed_seq = ck["seed_seq"]
140
+ self.model = PianoMind(self.n_tokens); self.model.load_state_dict(ck["state"]); self.model.eval()
141
+
142
+ @torch.no_grad()
143
+ def next_token(self, history, temperature=0.95, anti_silence=3, rep_penalty=1.5):
144
+ h = list(history)[-self.K:]
145
+ if len(h) < self.K:
146
+ h = [0] * (self.K - len(h)) + h
147
+ feat = torch.tensor([[x / self.n_tokens for x in h]], dtype=torch.float32)
148
+ logits, per, shared = self.model.telemetry(feat)
149
+ logits = logits / max(1e-3, temperature)
150
+ # keep it musical: don't collapse to silence, don't get stuck on one key
151
+ if anti_silence and all(t == 0 for t in h[-anti_silence:]):
152
+ logits[0] -= 8.0
153
+ for t in set(h[-3:]):
154
+ if t > 0:
155
+ logits[t] -= rep_penalty
156
+ tok = int(torch.multinomial(F.softmax(logits, -1), 1).item())
157
+ return tok, self.tok2midi.get(tok, 0), {"spec": per, "shared": shared}
158
+
159
+
160
+ if __name__ == "__main__":
161
+ model, meta = train_and_save()
162
+ # quick listen: generate 40 notes from the song's seed
163
+ p = PianoPlayer()
164
+ hist = list(p.seed_seq)
165
+ out = []
166
+ for _ in range(40):
167
+ tok, midi, _ = p.next_token(hist); hist.append(tok); out.append(midi)
168
+ print("sample MIDI stream:", out)
piano/poly_mind.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ poly_mind.py -- a CHORD-capable Modular Mind (same method as the boss / mono piano:
3
+ specialists -> RecursiveLink -> coordinator), but multi-LABEL: it predicts the SET of
4
+ notes sounding in the next frame, not a single note. Trained by multi-label next-frame
5
+ prediction on a polyphonic transcription (poly_notes.json, a piano-roll of chords).
6
+ """
7
+ from __future__ import annotations
8
+ import json, os
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+
14
+ HERE = os.path.dirname(os.path.abspath(__file__))
15
+ K, H, D_LATENT = 16, 64, 32 # context frames (=2s @8fps), hidden, latent
16
+ SPECS = [("Bass", "low"), ("Tenor", "mid"), ("Soprano", "high"),
17
+ ("Sustain", None), ("Onset", None), ("Phrase", None)]
18
+
19
+
20
+ def _group_masks(n):
21
+ m = {g: torch.zeros(n) for g in ("low", "mid", "high")}
22
+ third = max(1, n // 3)
23
+ for i in range(n):
24
+ g = "low" if i < third else ("mid" if i < 2 * third else "high")
25
+ m[g][i] = 1.0
26
+ return m
27
+
28
+
29
+ class PolyMind(nn.Module):
30
+ def __init__(self, n_notes):
31
+ super().__init__()
32
+ self.n = n_notes
33
+ nf = K * n_notes
34
+ self.fc1 = nn.ModuleList([nn.Linear(nf, H) for _ in SPECS])
35
+ self.lat = nn.ModuleList([nn.Linear(H, D_LATENT) for _ in SPECS])
36
+ self.drv = nn.ModuleDict({nm: nn.Linear(H, 1) for nm, o in SPECS if o})
37
+ self.ni = nn.LayerNorm(D_LATENT)
38
+ self.v = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
39
+ self.g = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
40
+ self.d = nn.Linear(2 * D_LATENT, D_LATENT, bias=False)
41
+ self.no = nn.LayerNorm(D_LATENT)
42
+ self.coord = nn.Linear(D_LATENT, n_notes)
43
+ for gname, t in _group_masks(n_notes).items():
44
+ self.register_buffer("mask_" + gname, t)
45
+
46
+ def _core(self, feat, tel=False):
47
+ drives = torch.zeros(feat.shape[0], self.n, device=feat.device)
48
+ lats, per = [], []
49
+ for i, (nm, o) in enumerate(SPECS):
50
+ h = torch.tanh(self.fc1[i](feat))
51
+ lat = self.lat[i](h); lats.append(lat)
52
+ dd = None
53
+ if o:
54
+ dd = self.drv[nm](h)
55
+ drives = drives + dd * getattr(self, "mask_" + o)
56
+ if tel:
57
+ per.append({"name": nm, "owns": o,
58
+ "drive": round(float(dd.mean().item()), 3) if dd is not None else None,
59
+ "act": round(float(lat.norm().item()), 3)})
60
+ z = torch.stack(lats, 0).sum(0); zn = self.ni(z)
61
+ shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
62
+ logits = drives + self.coord(shared)
63
+ return logits, per, shared
64
+
65
+ def forward(self, feat):
66
+ return self._core(feat)[0]
67
+
68
+ @torch.no_grad()
69
+ def telemetry(self, feat):
70
+ logits, per, shared = self._core(feat, tel=True)
71
+ return logits[0], per, [round(float(v), 2) for v in shared[0].tolist()]
72
+
73
+
74
+ def _rolls(seq, n):
75
+ R = np.zeros((len(seq), n), dtype=np.float32)
76
+ for i, fr in enumerate(seq):
77
+ for t in fr:
78
+ R[i, t] = 1.0
79
+ return R
80
+
81
+
82
+ def train_and_save(notes=os.path.join(HERE, "poly_notes.json"),
83
+ out=os.path.join(HERE, "poly_weights.pt"), epochs=700, seed=0):
84
+ torch.manual_seed(seed)
85
+ meta = json.load(open(notes)); seq, n = meta["seq"], meta["n_tokens"]
86
+ R = _rolls(seq, n)
87
+ X = np.stack([R[t - K:t].reshape(-1) for t in range(K, len(R))])
88
+ Y = np.stack([R[t] for t in range(K, len(R))])
89
+ X, Y = torch.tensor(X), torch.tensor(Y)
90
+ model = PolyMind(n)
91
+ opt = torch.optim.Adam(model.parameters(), lr=8e-3)
92
+ pw = torch.tensor(float((Y.numel() - Y.sum()) / (Y.sum() + 1))) # balance sparse positives
93
+ N, bs = X.shape[0], 512
94
+ for ep in range(epochs):
95
+ perm = torch.randperm(N); tot = 0.0
96
+ for i in range(0, N, bs):
97
+ idx = perm[i:i + bs]
98
+ loss = F.binary_cross_entropy_with_logits(model(X[idx]), Y[idx], pos_weight=pw)
99
+ opt.zero_grad(); loss.backward(); opt.step(); tot += loss.item() * len(idx)
100
+ if ep % 100 == 0 or ep == epochs - 1:
101
+ with torch.no_grad():
102
+ p = (torch.sigmoid(model(X)) > 0.5).float()
103
+ tp = (p * Y).sum(); prec = tp / (p.sum() + 1); rec = tp / (Y.sum() + 1)
104
+ print(f" ep {ep:4d} loss {tot/N:.3f} precision {prec:.2f} recall {rec:.2f}")
105
+ cnt = R.sum(1)
106
+ bi = int(np.argmax(np.convolve(cnt, np.ones(K), "valid")))
107
+ torch.save({"state": model.state_dict(), "n_tokens": n, "tok2midi": meta["tok2midi"],
108
+ "K": K, "fps": meta.get("fps", 8), "seed": [seq[bi + j] for j in range(K)]}, out)
109
+ print("saved ->", out)
110
+ return model, meta
111
+
112
+
113
+ class PolyPlayer:
114
+ def __init__(self, weights=os.path.join(HERE, "poly_weights.pt")):
115
+ ck = torch.load(weights, map_location="cpu")
116
+ self.n, self.K, self.fps = ck["n_tokens"], ck["K"], ck["fps"]
117
+ self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
118
+ self.seed = ck["seed"]
119
+ self.model = PolyMind(self.n); self.model.load_state_dict(ck["state"]); self.model.eval()
120
+
121
+ @torch.no_grad()
122
+ def next_frame(self, history, thresh=0.45, maxn=4):
123
+ h = list(history)[-self.K:]
124
+ while len(h) < self.K:
125
+ h = [[]] + h
126
+ R = np.zeros((self.K, self.n), dtype=np.float32)
127
+ for i, fr in enumerate(h):
128
+ for t in fr:
129
+ if 0 <= t < self.n:
130
+ R[i, t] = 1.0
131
+ feat = torch.tensor(R.reshape(1, -1))
132
+ logits, per, shared = self.model.telemetry(feat)
133
+ probs = torch.sigmoid(logits)
134
+ # anti-silence: if the last 2 frames were empty, force the most likely note(s)
135
+ if all(len(f) == 0 for f in h[-2:]):
136
+ thresh = min(thresh, float(probs.max()) * 0.6 + 1e-3)
137
+ idx = (probs > thresh).nonzero().flatten().tolist()
138
+ if len(idx) > maxn:
139
+ idx = torch.topk(probs, maxn).indices.tolist()
140
+ toks = sorted(int(t) for t in idx)
141
+ return toks, [self.tok2midi.get(t, 0) for t in toks], {"spec": per, "shared": shared}
142
+
143
+
144
+ if __name__ == "__main__":
145
+ train_and_save()
146
+ p = PolyPlayer()
147
+ hist = list(p.seed)
148
+ print("sample chords (MIDI sets):")
149
+ for _ in range(10):
150
+ toks, mid, _ = p.next_frame(hist); hist.append(toks); print(" ", mid)
piano/poly_notes.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"seq": [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [6], [6], [6], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [7], [6], [], [], [], [], [], [], [], [], [], [6], [9], [9], [9], [], [9], [9], [9], [9], [9], [], [9], [9], [9], [9], [9, 11], [9, 11], [9], [11], [], [3], [3, 6], [9], [11], [11], [11], [11], [], [11], [11], [11], [11], [1], [1], [1, 11], [1], [1], [1], [], [1], [1], [1, 13], [13], [1], [1], [1, 16], [1, 16], [1, 23], [], [13], [25], [25], [13, 25, 37], [4, 7, 13], [4, 19], [4, 19], [4], [4], [4, 23], [4, 23], [23], [14], [14], [14, 23], [4, 40], [4], [4, 33], [4], [4], [4, 14, 33], [14, 23], [14, 23], [4, 14, 23], [14], [], [2], [2], [2], [2], [2], [2], [2], [2], [2], [14], [2], [2, 24], [2, 24], [2], [2], [2], [2], [2], [2, 21], [14], [], [], [11, 15], [15], [15], [15], [15, 27], [11, 21], [11], [], [15], [15], [15], [15], [15], [15, 21], [], [], [], [15], [15], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 7, 26, 37], [4, 7], [4, 7, 28, 38], [7, 28, 31, 35], [7, 28, 31, 35], [7, 16], [7, 16, 28, 30], [19, 28, 30, 35], [7, 16, 28, 30], [7, 11, 28, 30], [9, 30, 37], [7, 11, 14, 30], [11, 14, 26], [11, 14, 26], [11, 14, 16], [11, 14, 16], [11, 14, 26], [11, 14, 18, 26], [11, 20, 26], [11, 14, 19, 31], [11, 19], [12, 19], [9, 13, 19], [9, 14, 19, 26], [12, 14, 19, 26], [12, 14, 19, 26], [4, 12, 26], [12], [12, 18], [11], [12, 16, 24], [11, 24], [11, 16], [11, 19], [18, 23], [11, 18, 23], [11, 17, 23], [11, 18, 23], [9, 18], [17], [9, 18], [9, 18], [9], [11, 19, 20], [19, 22], [19, 23], [19, 23], [19, 23], [19, 23], [11, 20, 43], [11, 21, 43], [7, 12, 21], [11, 43], [9, 11], [9, 18, 30], [9, 18, 30], [6, 9, 18, 30], [9, 12, 18, 30], [7, 18, 30], [7, 18, 30, 31], [7, 12, 18, 30], [6, 11, 28], [6, 11, 16, 28], [6, 11, 28], [11, 16], [16, 23, 28], [16, 23, 28, 30], [11, 16, 23, 28], [11, 16, 28], [11, 16, 28], [11, 29], [11, 16], [9, 16], [9, 19], [7], [6, 9], [6, 9, 18], [6, 9], [6, 9, 18], [6, 9, 21], [6, 9, 18], [6, 9, 18], [6, 9, 18], [6, 9], [6, 11, 19], [6, 11, 31], [23, 30, 33, 38], [30, 33, 35, 38], [7, 33], [7, 19], [6], [7, 16, 19], [6, 16, 26, 28], [6, 16, 26, 28], [16, 19, 26, 28], [7, 19], [7, 9, 22, 31], [28, 30, 36, 37], [28, 30, 33, 37], [12, 18, 28, 30], [9, 30], [7, 20], [12, 20, 24, 33], [12, 24, 28, 33], [12, 21, 28, 33], [7, 12, 21], [7], [7, 11, 15, 18], [11, 18], [30, 33], [6, 11, 15, 23], [9], [7, 9, 20, 32], [19, 21, 27, 33], [7, 33], [7, 11, 21, 28], [9, 11, 19], [9, 11, 19], [11, 19], [11, 30, 31, 40], [11, 30, 31, 40], [11, 30, 37], [11, 30], [19, 23, 31, 35], [11, 19], [11], [7, 11], [11, 18], [12, 18], [2, 18, 30, 33], [12, 18, 30, 33], [6, 12, 30, 33], [12, 26, 30, 33], [12, 26, 30, 33], [12, 18, 26, 33], [12, 26, 33, 42], [12, 16, 28, 38], [12, 28], [12, 27], [12, 18, 27, 34], [12, 18, 27, 34], [11, 18, 27, 34], [6, 11, 18, 26], [11, 18, 26], [11, 15, 18], [11, 15, 18], [11, 18, 27, 33], [11, 15, 18, 27], [11, 15, 18], [11, 15, 18], [11, 15, 18], [11], [6, 11], [11], [], [11], [11], [15], [15], [11, 15], [0, 3], [], [], [6], [], [], [], [], [21], [], [], [], [27], [23], [], [], [21], [], [], [], [6], [17, 19, 23], [4, 15, 19, 30], [4, 16, 19, 28], [16, 19], [4, 16, 19], [4, 19], [7, 15, 19], [16, 18, 28, 30], [7, 16, 18, 30], [18, 30, 35, 40], [18, 28, 30, 40], [7, 18, 28, 30], [7, 11, 14, 30], [7, 11, 18], [11, 16, 26], [16, 26, 28], [11, 16, 26], [11, 18, 26, 33], [14, 19, 26, 33], [11, 19, 26, 31], [11, 19, 26, 33], [11, 19, 26, 33], [9, 12, 19, 31], [12, 20, 26], [12, 14, 19], [12, 14, 19], [12], [10, 12, 18], [12, 18], [12, 16, 18], [12, 16], [11, 16], [11, 16], [11, 16], [11, 18], [11, 18], [11, 18, 30], [18, 33], [11, 18], [9, 11, 18], [9, 11, 18], [9, 11, 18, 30], [9, 11], [0, 9, 21, 33], [11, 19, 31, 35], [19, 31, 35, 38], [11, 31, 35, 38], [11, 19, 23, 35], [11, 23, 35], [11, 33], [11, 21, 33], [21, 24, 33], [11], [11, 25], [18, 24, 33], [24], [9, 18, 24], [6, 9, 18, 29], [9, 18, 23], [7, 23], [18, 23, 35, 42], [7, 23], [6, 16, 28], [6, 9], [6, 21], [4, 19, 30, 33], [23, 31, 35, 38], [23, 31, 35, 38], [32, 35], [19, 23, 33, 35], [21, 31, 33, 38], [19, 31, 33, 38], [21, 30, 33, 37], [7, 21, 28, 33], [16, 19, 21, 28], [6, 16, 21, 33], [6, 16, 21, 28], [6, 33], [6, 21], [6, 21], [6], [6], [6], [6], [6], [6, 14, 24], [7, 31, 33, 35], [31, 33, 35, 42], [31, 35], [7], [7], [6, 14, 17], [6, 16, 20], [6, 16], [7, 16], [7, 16], [6, 21, 31], [21, 30, 33, 36], [9, 30, 33, 40], [9, 30, 33], [9], [9], [7, 22], [24, 33, 35, 36], [24, 33, 35, 40], [7, 21, 24, 35], [19, 36], [7, 15, 17, 23], [7, 15, 18, 23], [23], [6, 15, 18, 23], [9], [9, 21, 33], [7, 21, 33], [7, 21, 33], [7, 21, 27, 33], [], [9, 15, 19, 23], [19, 31, 35], [23, 31, 35, 40], [31, 34, 35, 40], [23, 31, 35, 40], [19, 23, 31, 40], [11, 23, 31, 40], [11, 28], [11], [11, 16], [11, 16], [12, 23, 29, 35], [2, 12, 25, 42], [12, 26, 30, 33], [12, 27, 30, 33], [12, 27, 30, 33], [12, 27, 30, 42], [12, 26, 33], [12, 33, 42], [12, 24, 30, 33], [12, 16, 28], [12, 16, 28, 35], [11, 16, 23], [11, 15, 35], [11, 15, 18, 35], [12, 15, 18, 35], [12, 15, 23, 35], [15, 18, 23, 35], [15, 18, 23, 35], [11, 15, 18, 35], [15, 18, 22, 35], [15, 18, 23, 35], [15, 18, 23, 35], [11, 15, 22, 34], [11, 15, 23, 35], [11, 15, 35], [11, 15, 23], [23], [], [11], [11], [6], [], [8], [3, 6], [], [1], [], [5], [2], [21], [6, 23], [4, 23], [], [5], [], [], [], [0], [4, 10], [2], [1, 5, 10], [0, 10], [10], [0, 10], [4, 28], [4, 18, 28], [4, 20, 28], [4, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 26], [12, 20, 26, 32], [0, 27, 31], [0, 26, 31], [7, 12, 26, 30], [7, 12, 30], [0, 7, 12, 27], [12, 28, 30, 37], [7, 12, 27, 28], [2, 26, 28, 35], [2, 9, 38], [2, 30, 37, 38], [9, 26, 30, 37], [2, 9, 27, 30], [2, 26, 30, 37], [2, 26, 30, 37], [2, 14, 23, 30], [4], [4, 11, 22, 43], [4, 24, 38], [4, 16], [38], [4, 8, 11], [4, 11], [4, 9, 11], [18, 27], [20, 28], [16, 19, 28], [16, 19, 28], [2, 19, 28], [4, 19, 30], [4, 11, 16, 30], [4, 11, 19, 30], [0, 4, 18, 30], [0, 7, 19], [0, 7, 19, 30], [0, 16, 19, 31], [0, 16, 30], [0, 16, 19, 28], [7, 16, 28], [7, 16, 28], [9, 16, 26, 28], [9, 18, 26, 30], [18, 26, 30, 38], [18, 26, 30, 38], [9, 26, 30, 38], [2, 18, 26, 30], [2, 9, 18, 26], [4, 26, 40], [4], [4], [4, 28], [4, 11, 28], [4, 11, 28], [4, 8, 11], [4], [4, 30], [4, 30, 37], [4, 29, 31, 38], [4, 19, 31, 38], [19, 28], [4, 7, 19, 28], [4, 19, 28], [4, 11, 19, 28], [4, 19], [4, 7, 18], [7, 19, 26, 31], [0, 19, 26, 31], [0, 19, 26], [0, 4, 26], [0, 18, 24], [0, 7, 18, 24], [0, 18, 24], [2, 9, 22, 30], [9, 18, 23, 30], [2, 9, 23, 30], [9, 18, 23, 30], [9, 18, 21, 30], [2, 18, 21], [9, 18, 21, 30], [4, 16, 19, 21], [4, 38], [4, 38, 40], [4], [4, 11, 28, 35], [4, 11], [4], [4], [4], [4, 19, 30], [31, 35], [2, 20], [4, 7, 19, 24], [4, 7, 19, 23], [19, 28], [4, 11, 19, 28], [4, 18, 28], [0, 3, 19, 30], [0, 19], [0, 16, 19, 31], [0, 16, 30, 31], [0, 16, 30], [0, 16, 28, 30], [0, 7, 16], [4, 16, 28], [9, 18, 28], [9, 18, 30], [2, 9, 18, 30], [2, 9, 18, 30], [4, 9, 18, 26], [9, 18, 27, 30], [9, 18, 30], [9, 16, 26], [4, 11, 26, 28], [11, 28, 35, 40], [3, 11, 28], [1, 11, 28], [11, 27, 28, 35], [4, 27, 28], [4, 8, 28], [4, 28], [4, 19, 31, 38], [4, 19, 31, 40], [28, 31, 38, 40], [4, 28, 31, 40], [4, 28, 31, 40], [28, 31, 40], [11, 19, 31, 38], [4, 19, 27], [4, 19, 31, 38], [0, 26, 31, 38], [0, 31, 38], [0, 26, 31, 38], [26, 31], [7, 19, 26, 31], [0, 7, 19, 26], [0, 7, 16], [6, 9, 29], [9, 30], [2, 9, 18, 26], [2, 9], [2, 6, 9], [2, 9, 18, 24], [2, 9, 24], [2], [4, 24], [4, 23], [4], [4, 11], [4, 11], [11, 29, 35], [16, 28, 35], [7, 14, 16], [30, 38, 43], [4, 31, 38, 40], [4, 31, 38, 40], [4, 28, 31, 38], [4, 28, 31], [4, 30, 31], [4, 16, 30, 31], [4, 19, 30], [0, 4, 31], [0, 31, 38], [16, 31, 35, 38], [0, 31, 42], [2, 7, 16], [16, 19, 27, 28], [28, 31, 38, 40], [0, 31, 38, 40], [18], [26, 30, 36], [18, 26], [2, 4, 18, 30], [2, 4, 9], [2, 8, 18, 28], [2, 18, 28], [2, 4, 37], [4, 16, 28], [4, 16, 19, 28], [4, 19, 28], [4, 11], [4, 19], [4, 20, 29], [4, 19], [4, 8], [4, 31], [4, 19, 31], [4, 18, 31], [19, 28], [4, 11, 19], [4, 19, 29], [19, 28], [4, 19, 27], [0, 4, 7, 31], [0, 7, 31], [0, 7, 31, 38], [27, 31, 38], [26, 31], [0, 7, 43], [0, 24], [0, 7, 10], [2, 21, 28, 33], [9, 30, 35, 37], [2, 9, 30, 35], [30, 33, 35, 42], [9, 21, 29, 33], [2, 6, 18, 22], [2, 9, 19], [2, 16, 19], [4, 19], [4, 16, 19], [4, 19], [4, 11, 19], [11, 19, 23], [4, 8, 11], [4, 38], [4, 7, 16, 18], [4, 11, 19, 31], [11, 31], [2, 19, 31, 38], [7, 19], [2, 4, 7], [1, 4, 19, 28], [4, 19, 28], [19, 28, 29], [0, 4, 19], [19, 31, 38], [0, 7, 31], [0, 30, 31], [0, 16], [0, 16, 20, 29], [0, 16], [0, 7, 16], [9, 29, 30], [2, 9], [2, 9, 18], [9, 18, 26], [4, 18, 26], [2, 18, 26], [2, 10, 18, 28], [3, 18], [4], [4, 16], [4, 29], [0, 11, 16, 28], [11, 16, 28], [4, 11], [4], [4, 7, 12], [4, 28, 31], [19, 28, 35, 38], [4, 19, 28, 31], [4, 28, 31, 35], [4, 6, 16, 19], [4, 7, 19, 31], [30, 33, 37, 40], [18, 21], [4, 11, 19, 21], [7, 11, 19, 23], [19, 24, 31, 36], [19, 23, 35], [11, 23, 31, 37], [19, 23, 31, 38], [19, 23, 31, 38], [11, 31, 38], [12, 31, 32, 33], [4, 33, 36, 40], [24, 33, 36, 40], [12, 28, 33, 40], [12, 19, 28, 33], [23, 28, 31, 33], [11, 28, 31, 35], [11, 28], [4, 18, 21], [18, 21, 26], [6, 18, 22, 27], [6, 18, 21, 27], [18, 23], [6, 9, 18], [9, 18], [11, 16, 18, 22], [17, 22, 29], [19, 23, 29, 31], [7, 19, 23, 31], [7, 19, 31, 35], [7, 23, 28, 31], [19, 23, 28, 31], [19, 23, 29, 31], [11, 19, 20, 27], [3, 6, 9, 30], [9, 21, 23, 30], [30, 32, 35, 40], [7, 28, 31, 35], [2, 7, 9], [7, 15, 18], [6, 15, 18, 23], [6, 9, 15], [7, 19, 27, 30], [4, 19, 28, 31], [4, 28, 31, 35], [4, 28, 35, 38], [4, 19, 28, 35], [4, 19, 28], [4, 28, 35, 38], [4, 19, 21, 28], [4, 18, 27, 30], [6, 15, 27], [6, 15, 27, 43], [6, 27, 43], [6], [6], [6], [6], [2, 15, 18], [16, 19], [6, 16, 18, 23], [6, 15, 23], [6, 15, 19], [6, 28, 35, 38], [9, 28], [7, 13], [4, 9, 33, 41], [30, 33, 37, 40], [19, 24, 29, 33], [9, 16, 19], [19, 28, 31, 38], [4, 19, 28, 38], [7, 19, 28], [4, 7], [7, 30, 33], [6, 28, 30, 33], [6, 20, 29], [6, 16, 28, 35], [6, 11, 16, 35], [18, 27, 30, 33], [27, 30, 33], [6, 21, 33], [4, 29, 30, 33], [23, 28, 31, 35], [11, 28, 31, 35], [11, 28, 31, 35], [11, 31], [4, 6, 11, 31], [11, 19, 31], [6, 11], [12, 19, 23], [12, 21, 24], [6, 12, 24, 28], [12, 24, 28, 33], [12, 24, 28, 33], [12, 28, 33], [12, 21, 28, 33], [7, 12], [3, 20, 33, 35], [27, 30, 35], [30, 35], [2, 30, 35], [30, 35], [11, 28, 30, 34], [11, 27, 30, 35], [23, 27, 30, 35], [11, 27, 30, 34], [11, 27, 30], [11, 28, 30], [22, 28, 30, 35], [3, 11, 30], [30, 35], [11, 18, 30, 35], [11, 30, 35], [4, 18, 30, 33], [3], [3, 10], [3, 10, 33], [3, 10], [2, 10], [3, 10], [3, 10], [2], [3, 10], [3, 10], [3, 10], [3, 10, 33], [3, 10], [3, 10], [3, 10], [4, 10], [3, 10], [3, 10], [3, 10], [3, 10], [2, 10], [9], [8], [4, 30, 31, 34], [19, 31, 35], [4, 23, 31, 38], [4, 18, 31, 38], [7, 18, 23, 35], [4, 18, 21, 30], [30, 33, 35, 37], [4, 11, 30, 33], [4, 18, 21, 33], [19, 23, 35], [11, 20, 23, 35], [11, 20, 23, 35], [19, 23], [11, 23, 35, 42], [7, 11, 19], [11, 18, 20, 23], [4, 12, 22, 35], [12, 33, 36], [12, 24, 33, 36], [9, 12, 33, 36], [9, 12, 19], [19, 23, 35, 40], [11, 31, 35, 38], [11, 21], [3, 11, 18, 21], [11, 19, 21], [9, 11, 19, 21], [11, 19, 21], [6, 21], [6, 9, 21], [9, 21], [9, 18], [0, 18, 21], [7, 19, 23, 35], [19, 23, 31, 42], [11, 19, 35, 42], [19, 23, 30, 35], [11, 19, 23], [11, 19, 23, 35], [11, 18, 26, 35], [4, 9, 18, 33], [9, 18, 21, 33], [28, 32, 33, 35], [28, 31, 35, 38], [7, 28, 31], [2, 6, 15, 17], [6, 9, 15, 30], [6, 15, 18], [4, 27, 30], [16, 28, 31, 38], [16, 19, 28, 31], [4, 19, 28, 31], [19, 28, 31, 33], [19, 28, 31], [7, 28, 31, 38], [4, 19, 28, 31], [6, 27, 30, 35], [6, 27, 30, 35], [6, 26, 30, 35], [6, 30, 35], [6, 30, 35], [6, 35], [6], [6], [14, 17, 19], [16, 19, 43], [7, 15, 19], [6, 9, 15, 18], [6, 9, 15, 31], [28, 31, 35, 38], [7, 31], [7, 16], [28, 31, 33, 38], [21, 30, 33, 37], [16, 30, 33], [16, 33], [19, 28, 31, 35], [16, 28, 31, 38], [4, 9, 19, 31], [4, 7, 16], [23, 34, 36, 41], [30, 33, 37, 40], [20, 23, 33, 40], [19, 23, 40], [6, 11], [0, 19, 21], [11, 24, 33], [6, 19, 24], [4, 36, 43], [31, 35, 40], [31, 35, 38, 40], [20, 28, 35, 40], [11, 28, 35, 40], [28, 35, 42], [6, 28, 35, 42], [6, 23], [12, 16, 24, 29], [18, 25, 30], [6, 18, 24, 40], [18, 24, 30, 40], [18, 24, 36, 40], [0, 18, 24, 40], [12, 18, 24, 28], [12], [4, 24], [11, 15, 27, 35], [11, 27, 35], [11, 15, 27, 35], [3, 11, 27, 35], [11, 15, 27, 35], [11, 23, 27, 35], [27, 35, 42], [8, 11, 27, 35], [3, 23, 27, 35], [3, 27, 35], [3, 27, 35], [23, 27, 35], [15, 26, 35, 42], [15, 23, 26, 35], [23, 27, 35], [2, 6, 15, 23], [11, 27, 35], [6], [], [], [6, 15], [15], [6, 15], [6], [], [], [], [], [2, 9], [1], [11], [3, 5, 11], [11], [], [], [], [], [35], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 40], [4], [4], [40], [], [40], [40], [40], [], [40], [], [], [], [40], [40], [], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4], [], [4], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 40], [4, 40], [4], [], [], [], [40], [40], [], [40], [33, 40], [], [], [40], [40], [40], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4], [4], [4], [], [], [], [], [], [], [9], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [40], [], [], [40], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [8], [8], [8], [8], [0, 8], [8], [2, 5, 8], [4, 7, 11, 23], [4, 8, 11], [4, 7, 11], [4, 8, 11], [4, 8, 11], [3, 7, 19], [4, 8, 11], [5, 7, 11, 40], [4, 8, 11], [4, 7, 11], [6, 9, 14, 21], [1, 9, 16, 21], [16, 21], [1, 10, 16, 21], [10, 16, 20, 21], [4, 16, 20, 21], [4, 7, 11, 20], [8, 16, 19], [4, 8], [4, 8, 20], [8, 19, 28], [4, 7, 11, 16], [4, 8, 11, 18], [8, 18], [4, 7, 11, 18], [4, 18, 30], [10, 19, 33], [9, 14, 21], [16, 22], [1, 10, 16, 21], [1, 10, 16, 21], [1, 7, 16, 19], [4, 7, 19, 28], [8, 16, 19, 28], [7, 16, 19, 28], [3, 16, 19], [8, 16, 19], [0, 4, 7], [8, 16, 19], [0, 7, 16], [4, 7, 11], [4, 8, 11, 16], [0, 10, 16, 33], [1, 16, 28, 42], [16], [10, 16, 28], [16, 28], [2, 8, 28], [4, 7, 19, 28], [8, 16], [6, 11, 16], [4, 11, 16], [4, 8, 28], [2, 7, 11, 16], [5, 7, 16], [1, 8, 16], [4, 7, 11], [4, 8, 43], [1, 10, 33], [5, 9], [6, 9], [1, 10], [0, 10], [0, 5, 10], [4, 7, 11], [4, 8], [4, 8, 19, 40], [0, 4, 8], [8, 23], [4, 7, 11, 43], [8, 11], [4, 7], [4, 7, 11, 23], [0, 4, 8], [1, 9, 15, 20], [10, 16, 21, 43], [16, 21], [1, 10, 16, 21], [2, 10, 16, 21], [3, 8, 16, 19], [4, 7, 19, 28], [8, 16, 19, 23], [7, 16, 19, 28], [4, 16, 19, 23], [16, 19], [4, 7], [4, 8, 16, 18], [3, 8, 16, 18], [4, 7, 16, 30], [4, 18, 30, 40], [5, 9, 15], [2, 16, 22, 24], [3, 16, 24, 28], [0, 10, 30], [10], [0, 5, 23], [7, 16, 23, 28], [8, 16, 23], [4, 7, 16, 23], [0, 16, 23], [16, 19, 23, 28], [4, 7, 19, 28], [8, 16, 19, 28], [8, 19, 23], [4, 7, 11, 19], [4, 9, 18], [10, 15, 18, 21], [16, 18], [16, 18], [10, 16, 18, 30], [0, 16, 18, 30], [8, 16, 18, 28], [4, 16, 19], [4, 8, 16], [4, 7, 40], [4, 8, 11, 16], [4, 8, 16], [4, 7, 11], [1, 4, 8, 16], [4, 8, 16], [4, 7, 11], [4, 9, 16], [6, 9, 16, 33], [5, 9, 16], [2, 6], [0, 10, 33], [0, 5, 10], [0, 6, 10, 33], [9], [0], [1, 6, 10], [1, 10], [0, 10], [4, 7], [16, 19, 28, 31], [16, 19, 28, 31], [4, 7, 19, 28], [8, 19, 28, 31], [4, 7, 28, 31], [4, 7, 20, 28], [8, 16, 20, 28], [4, 7, 19, 28], [4, 11, 19], [2, 5, 8, 16], [10, 17, 20, 30], [18, 21, 30], [18, 21, 30, 33], [21, 30, 33, 36], [10, 30, 33], [4, 7, 28, 31], [19, 28, 31, 35], [19, 28, 31, 35], [4, 28, 31, 35], [16, 19, 28, 31], [4, 8, 28, 43], [8, 19, 28, 35], [19, 28, 35], [4, 7, 19], [4, 11, 18], [2, 5, 18, 27], [15, 27, 30, 37], [27, 30, 34, 37], [1, 10, 27, 30], [1, 10], [33], [4, 7, 16], [1, 8, 16], [7, 16], [4, 11, 16], [16, 19, 43], [0, 7, 16], [4, 8, 23], [4, 40], [3, 7, 11], [0, 23, 43], [6], [4, 8, 11, 19], [4, 8], [4, 6, 11, 40], [4, 8, 11], [4, 9], [0, 10, 19, 36], [9], [1, 21, 33], [10, 16, 21], [21], [2, 7, 16, 31], [7, 16, 19, 31], [16, 19, 31], [4, 7, 11, 31], [7, 19, 31], [7, 31], [4, 11, 18, 30], [3, 8, 18, 30], [1, 6, 30], [4, 11, 18, 30], [9, 22], [10, 19, 31, 33], [1, 4, 21], [2, 33, 40], [1, 10, 28, 33], [10, 22], [4, 7, 19], [19], [8, 16, 19], [7, 11, 19, 28], [0, 4, 19], [7, 19], [4, 7, 19, 30], [8, 18, 30], [0, 3, 6, 8], [4, 8, 18, 30], [9, 19, 38], [0, 10, 16], [10, 16, 28], [16, 24, 28], [1, 10, 16, 28], [16, 28, 35, 36], [4, 7, 12, 16], [11, 16, 23, 28], [8, 16, 23, 28], [4, 7, 16, 28], [0, 4, 16, 23], [4, 11, 16, 23], [4, 7, 11, 16], [8, 11, 16], [3, 7, 11, 16], [4, 8, 11, 16], [4, 11, 16], [0, 10, 16, 33], [0, 10, 16], [9, 16], [10, 16], [10, 16, 36], [7, 11, 16], [4, 8, 11], [8], [4, 6, 11, 40], [0, 4, 8], [7, 23], [4, 7, 33], [8, 16, 23, 35], [3, 6, 35], [4, 23, 35], [0, 4, 11, 35], [0, 10, 21, 33], [16, 21, 43], [9, 21, 43], [10, 21, 43], [4, 21, 23, 43], [4, 7, 19], [11, 19, 28, 31], [8, 19, 23, 31], [7, 11, 31], [0, 8, 23], [8, 19, 31], [4, 7, 11, 18], [4, 8, 18], [3, 7, 18, 30], [4, 7, 11, 18], [4, 19, 21], [0, 3, 10, 33], [2, 31, 35, 36], [1, 31, 36, 40], [0, 31, 36, 40], [31, 35, 36], [4, 7, 35, 43], [4, 30, 35], [8, 23, 30, 35], [7, 28, 35, 40], [0], [19], [4, 8, 11], [8, 16, 31], [0, 3, 19, 31], [4, 8, 11, 31], [4, 7, 31], [3, 10, 18], [4, 9, 18, 30], [2, 10, 18, 30], [0, 10, 18, 30], [4, 33], [2, 7, 12, 16], [11, 16], [4, 8, 16, 28], [7, 11, 16, 28], [1, 11, 16, 28], [8, 16, 19], [7, 11, 16, 28], [8, 16, 23, 28], [4, 7, 11, 16], [4, 8, 11, 16], [4, 11, 16, 28], [10, 16], [0, 9, 16, 28], [11, 16, 28], [0, 10, 16], [10, 16], [3, 6, 10, 28], [16], [1, 40], [0, 3, 6, 10], [0, 3, 6, 10], [7, 9], [4, 11, 23, 31], [4, 8, 28, 31], [4, 7, 19, 31], [4, 19, 28, 31], [8, 19, 28, 31], [4, 7, 28, 31], [4, 8, 28, 31], [4, 19, 28, 31], [11, 19, 28, 35], [0, 4, 9], [2, 5, 16, 19], [21, 36, 37], [6, 21, 37], [3, 6, 10, 37], [6, 9, 11, 37], [5, 8], [4, 7], [4, 8, 11, 31], [7, 16], [4, 11, 19], [4, 11, 43], [1, 7, 16], [1, 16, 19], [4, 7, 10, 28], [4, 10, 19], [10, 19], [4, 10, 27, 30], [4, 8, 30, 33], [2, 8, 33, 42], [2, 18], [4, 11, 33], [8, 16, 33], [4, 16, 28], [4, 11, 16], [2, 7, 16, 23], [4, 16, 23, 28], [4, 11, 16, 23], [2, 7, 11, 16], [], [], [40], [], [], [], [40], [6], [], [26, 38], [26, 38], [], [23], [], [], [4, 19], [9, 19], [19], [21], [21], [], [], [18], [19], [19], [19], [19], [], [23], [11, 23], [11, 23], [], [7, 16], [7, 16], [16], [16], [], [], [19], [6, 21, 40], [21, 40], [40], [19], [19], [], [21], [21, 40], [7, 23], [7, 40], [7, 40], [30], [31], [30], [31], [], [6, 27], [27], [27], [30], [30], [40], [40], [40], [4, 7, 23], [4, 7, 23], [4, 7, 23], [23], [], [], [], [], [6, 11], [11, 30], [30], [30], [30], [30], [40], [30], [4, 9], [9], [9], [9, 23], [9, 23], [9], [9], [26], [4, 9], [26], [26, 38], [], [24], [23], [23], [23, 40], [4, 7], [7, 23], [7, 23], [19], [19], [], [], [], [6], [21], [40], [19], [19], [19], [30, 37], [], [4, 7, 23], [4, 23], [23], [23], [23], [23], [23], [23], [6], [27], [27], [40], [], [30], [30], [], [0, 6], [], [], [32], [32], [], [], [], [8], [8], [8], [8, 40], [28], [32], [32], [32], [6, 23], [23], [23], [23, 40], [28, 40], [], [30], [], [], [], [], [6], [30], [32], [32], [32], [37], [], [], [], [30], [], [], [], [4, 8, 32], [8, 32], [32], [], [], [40], [40], [40], [6, 23], [9, 23], [23], [23], [23], [23, 25], [23, 25], [], [5, 23], [23], [23, 25], [25], [23, 25], [], [23, 30], [30], [6], [30], [], [25], [25], [25], [32], [32], [8, 32], [8, 32], [], [25], [25], [25], [], [25], [6, 11], [], [25], [25], [25], [], [35], [35], [6, 35], [35], [], [25], [25], [], [25], [37], [6], [], [], [], [37], [], [35], [35], [4, 8], [4], [35], [], [37], [37], [38], [11, 38], [6, 21], [6], [37], [37], [37], [37], [37], [], [1, 5, 35], [1, 35], [1, 35], [1, 35], [8, 35], [1], [5, 35], [8, 13, 16], [0, 20, 29, 36], [10, 30, 37], [18, 21, 30, 33], [21, 30, 33, 37], [6, 9, 30], [0, 21, 23], [3, 8, 11, 23], [11, 20], [3, 6, 9, 23], [9, 23, 24], [4, 9, 25], [4, 9, 13, 25], [4, 9, 25], [1, 8, 25, 33], [2, 8, 21, 25], [13, 21, 25, 33], [6, 25, 33], [14, 26, 35, 38], [14, 26, 35, 38], [6, 14, 26, 35], [6, 14, 35], [0, 9, 21, 26], [9, 21, 25], [2, 13, 21, 25], [4, 8], [1, 8, 11, 23], [8, 20, 24], [8, 20, 23, 32], [0, 4, 8, 20], [2, 8, 11, 20], [5, 11, 20], [4, 11], [9, 19, 24, 31], [9, 26, 33, 40], [9, 25, 33, 40], [9, 25, 33, 40], [9, 26, 33, 40], [21, 26, 33, 40], [25, 33, 40], [9, 13, 25], [4, 20, 23], [3, 11, 20], [11, 18, 21, 37], [18, 21, 30, 33], [6, 11, 18, 21], [4, 8, 17], [4, 11, 17, 29], [4, 8, 11], [6, 29, 32, 36], [6, 18, 30, 33], [6, 21, 30, 33], [6, 21, 30, 33], [6, 21, 30, 33], [0, 9, 30, 33], [9, 21, 30, 33], [21, 30], [6, 8, 17, 33], [3, 17, 29, 32], [17, 20, 29], [8, 11, 17, 29], [8, 11, 17, 29], [2, 8, 17, 29], [3, 8], [5, 8, 29], [4, 9, 20, 32], [4, 30, 33, 37], [4, 9, 30], [4, 9, 17, 29], [9, 17, 20, 29], [4, 9, 30], [4, 21, 30, 33], [4, 8, 18, 21], [6, 11, 21, 32], [6, 11, 20, 32], [6, 11, 20, 32], [6, 11, 32], [11, 18, 21, 30], [1, 5, 18, 21], [18, 21], [9, 19, 24, 36], [1, 8, 32, 35], [1, 8, 32, 35], [1, 8, 22, 35], [8, 18, 30, 37], [8, 30, 33, 37], [8, 23, 32, 36], [20, 23, 32, 35], [1, 11, 20, 24], [4, 8], [1, 8, 25], [1, 8, 25], [1, 9, 25], [0, 25, 33], [1, 8], [0, 8, 25], [13, 21, 22, 26], [9, 25, 31, 33], [9, 14, 25], [9, 26, 35, 42], [9, 14, 26, 35], [9, 14, 26, 35], [9, 14, 23, 26], [9, 23, 26, 33], [14, 20, 23], [2, 6, 8, 32], [8, 25, 32, 37], [1, 8, 32, 37], [8, 25, 32, 37], [8, 25, 32, 37], [8, 20, 32, 37], [0, 32, 37, 39], [25, 32, 37, 39], [5, 9, 32, 37], [20, 25, 32, 37], [5, 9, 32, 37], [5, 9, 25, 32], [9, 20, 25, 32], [2, 9, 25, 32], [9, 25, 32], [2, 9, 25, 32], [5, 20, 26, 32], [20, 25, 32], [17, 25, 32], [17, 20, 25, 32], [0, 5], [0, 5, 9], [5, 9, 11], [5], [2, 5, 9, 11], [5, 9], [5, 9], [5, 9], [2, 5, 9, 11], [2, 5, 9, 11], [0, 9], [2, 5, 9], [4], [5], [5, 11], [5, 11], [5], [5, 11], [5, 11], [5, 11], [10, 27, 30], [10, 18, 30, 37], [1, 10, 18, 30], [1, 25, 30, 37], [0, 10], [0, 4, 20, 25], [3, 8, 11, 20], [8, 11, 20, 23], [4, 6, 9, 20], [4, 9, 25], [9, 21], [9, 13, 25], [4, 9, 25, 30], [8, 21, 25, 30], [1, 8, 21, 25], [8, 21, 25, 37], [3, 11, 33, 40], [6, 11, 35, 39], [14, 23, 35, 38], [6, 23, 35, 38], [6, 11, 14], [6, 11, 22, 27], [6, 11, 21], [6, 11, 19, 25], [4, 6, 11, 20], [1, 8, 11], [1, 8, 11, 35], [8, 23], [8, 11, 23, 35], [8, 11, 23, 32], [11], [3, 11], [4, 9, 20, 27], [9, 25], [9, 25, 37], [9, 37], [9, 37], [9, 25, 33, 37], [2, 9, 25, 33], [1, 9], [6, 20, 23, 25], [4, 11, 18, 20], [4, 11, 18], [4, 11, 18, 33], [2, 18, 21, 33], [3, 17, 20, 29], [11, 20, 29, 32], [6, 11, 29, 32], [6, 17, 29, 32], [6, 21, 31, 38], [6, 30, 33, 40], [6, 21, 33, 40], [6, 29, 33, 40], [2, 6, 33, 40], [10, 21, 30, 40], [10, 17, 32, 40], [3, 8, 17, 29], [8, 17, 29, 32], [8, 17, 20, 29], [8, 11, 20, 32], [6, 8, 11, 32], [2, 9], [2, 9], [3, 8], [4, 9, 31], [4, 30, 33, 40], [4, 9], [9, 17, 29, 32], [9, 17, 20], [2, 9, 30, 33], [0, 8, 30, 33], [2, 8, 18, 33], [4, 11, 30], [11, 20, 32, 35], [11, 20, 32, 35], [11, 35], [6, 11, 21], [18, 21, 33], [18, 21], [3, 9, 11], [0, 23, 35, 42], [1, 8, 11, 35], [1, 8, 11, 18], [8, 25, 30, 37], [8, 18, 30, 33], [0, 8, 20], [8, 26, 32, 38], [8, 28, 31, 38], [0, 4, 8, 20], [1, 8, 25], [8, 25], [8, 25], [8, 22, 25], [8, 21, 25], [21, 25], [4, 25, 26, 29], [2, 9, 28], [2, 9, 23], [9, 14, 23, 35], [9, 14, 23, 38], [9, 23, 26, 35], [9, 14, 23, 35], [9, 23, 26, 38], [9, 14, 20, 26], [4, 8, 20, 26], [1, 8, 25, 32], [8, 32, 37], [8, 37], [8, 32, 37], [5, 8, 32], [0, 20, 32, 37], [20, 25, 32], [5, 9, 32, 37], [5, 20, 32, 39], [2, 32, 37, 39], [2, 20, 25, 32], [4, 8, 25, 32], [2, 25, 37, 39], [6, 20, 32, 37], [20, 32, 37], [3, 13, 20, 32], [5, 13, 37], [5, 37], [25], [], [], [], [11], [5, 9, 25], [2, 5], [0, 4, 11], [1, 4, 11], [1], [1], [0], [3], [4, 13], [], [5], [1], [13], [1], [1, 5], [13], [2, 6, 13], [6, 25, 32], [25, 32], [1, 25, 33], [1, 21, 25, 33], [6, 13, 21, 33], [33], [13, 21, 33], [9, 13, 32], [9, 21, 33], [], [6], [], [32], [13, 20, 39], [], [9], [9, 16], [18, 21, 30], [30], [18, 21, 30], [21, 30], [29], [], [], [], [], [9], [9], [18, 30, 33], [18, 21, 30, 33], [18, 30, 33, 40], [19, 22], [20, 22], [20, 23], [6, 20, 23], [20, 23], [20, 23], [20, 23], [20, 23], [20], [20, 21], [20, 21], [20, 21], [21], [21], [21], [21], [20, 29], [17, 20, 29, 36], [29, 32], [17, 20, 29], [33], [33], [29], [29], [], [], [], [], [], [], [], [], [23], [20, 32, 37], [21, 24, 33], [6, 21, 33], [25, 33], [21, 33], [21, 33], [33], [33], [20, 32, 35], [23, 32, 35], [20, 32], [], [21], [6, 18, 21], [18, 21, 33], [16], [16, 20], [15, 32], [4, 16, 32], [4, 16], [16], [], [16], [], [20], [21, 33], [8, 21, 33], [8, 21, 33], [11, 21, 33], [11, 21, 33, 40], [12], [6, 20], [13, 25], [21, 25, 32, 33], [6, 25, 32, 33], [25, 33], [25, 32, 40], [9, 25, 32, 33], [32, 33], [13, 39], [13, 33], [13, 21, 33], [13, 39], [21, 25, 32], [6, 21, 26, 33], [14, 26], [], [8], [8, 20, 25, 32], [12, 20], [32], [33], [33], [], [], [], [], [], [], [], [], [], [], [9, 21], [9, 21, 24], [21, 25], [21, 24], [21, 24], [21], [9, 25], [9, 19, 21], [18, 21, 33], [18, 21, 30, 33], [18, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18, 33], [23], [19, 23], [20, 23], [21, 23], [6, 20], [37], [21, 25, 33, 37], [33], [11], [21, 24], [23, 26], [23, 26], [26], [26], [21, 25], [21, 25], [21, 25, 33], [21, 33, 37, 40], [21, 33, 37, 40], [25, 33, 40], [33, 40], [25, 33], [8, 20, 26], [8, 21, 26, 33], [8, 21, 25, 33], [8, 13, 26], [8, 13], [13, 20], [8, 20, 23], [8, 20, 23, 35], [8, 20, 23], [8, 23], [], [21, 30, 33], [18, 21, 33], [32, 33], [6, 32], [], [], [], [], [18, 21], [18, 21, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18], [6, 18, 30], [20, 32, 35], [20, 23, 32, 35], [4, 20], [20, 23], [20, 23, 35], [20, 23], [23, 35], [8, 23], [8, 23], [8, 20], [8, 21, 23], [20, 32, 35], [20, 32], [8, 17], [8, 17], [17, 29], [17, 20, 29, 32], [], [1], [13, 17, 20, 32], [13, 20, 29, 32], [20, 29, 32, 36], [20, 29, 32], [20, 29, 32, 36], [17, 20, 29, 32], [29, 32], [16, 20], [13, 17, 32], [29], [16, 32], [17, 32], [17], [16], [], [], [], [], [], [], [], [], [41], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [0, 3, 7, 11], [7, 11], [7, 11], [4, 7, 11], [4, 7, 11], [4, 7, 10], [7, 11], [7], [7, 11], [], [11], [7, 11], [11], [7, 11], [6, 11], [7, 16], [7, 16, 28], [7, 16], [16], [16], [7, 16, 28], [7, 16, 28], [16], [7, 16, 28, 35], [7, 16, 28], [16, 28], [], [7], [7], [7, 16], [7, 16, 28], [7, 16], [7, 9, 16], [7, 16, 18], [9, 18], [4, 9, 18], [9, 18], [9, 18], [9, 18], [18], [7, 18], [7, 18, 30], [7, 18], [7, 18], [7, 18], [7, 18], [7, 18], [], [6, 14], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [6, 9, 15], [6, 9, 15], [15], [15], [15], [9], [9], [9], [3], [], [10, 19], [11, 31, 38], [11, 19, 31], [19, 31, 35, 38], [19, 31, 35, 38], [11, 19, 31], [11, 31], [], [9, 19, 30, 31], [9, 18, 30], [9, 18, 28, 30], [9, 18, 30], [], [7, 15], [7, 16], [7, 12, 16], [6, 14], [2, 6, 14, 26], [6, 14, 26], [6, 14, 26], [6, 14, 26], [14, 26], [6, 14, 26], [6, 14], [6, 14], [], [], [9], [], [6, 10, 12], [6, 12], [11], [6, 11], [4, 7, 11], [4, 7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 9, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [], [], [], [], [7, 12, 21, 23], [7, 14, 23], [14, 23], [14, 23], [23], [23], [11], [11, 19], [11, 19], [11, 19, 31], [11, 19], [11, 19, 31], [11, 19, 31], [11, 19], [11, 19], [], [11, 19, 21], [12, 21], [21], [21], [], [23, 26], [23], [], [23, 24], [16, 24, 28, 36], [16, 24, 28], [16, 24, 28, 36], [36], [14, 23], [23], [23], [23, 26], [23], [23, 35, 42], [6, 23, 35], [23], [23], [23, 35], [23], [23], [23, 35], [11, 21], [12, 21], [12, 21], [12, 21], [6, 12, 21], [21], [11, 19], [11, 19], [11, 19], [7, 11, 19], [11], [], [], [11], [11, 19, 31], [11, 19, 30, 31], [11, 19, 31, 35], [11, 19, 31, 35], [11, 19, 31, 38], [11, 21, 33, 43], [12, 21, 31, 33], [12, 21, 33], [2, 12, 21], [12, 21], [12, 21, 33], [6, 12, 21, 33], [12, 21, 33], [12, 21, 33], [12, 21], [12, 21], [21, 24, 33], [31, 33], [31, 33], [33], [], [6], [30, 37], [30], [11], [11, 18, 30], [11, 18, 30], [11, 30, 37], [11, 18, 30, 37], [18, 30], [11, 18, 30, 37], [11, 30, 37], [11, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [5, 10, 30, 37], [3], [], [], [2, 5], [], [], [], [10], [10], [3], [3, 10], [3, 10], [3, 10], [10], [3, 10], [2, 9], [3, 10], [3, 10], [3, 10], [3, 10], [3, 5, 10], [3, 10], [3, 10], [3, 10], [3, 10], [3, 6, 10], [2, 10], [5, 10], [2, 5, 10], [3, 10], [3, 5, 10], [4, 30], [4, 35], [4, 19, 35], [23, 31, 35, 38], [3, 6, 35, 38], [4, 8, 23, 35], [4, 23, 31, 35], [4, 23, 35], [2, 23, 31, 35], [2, 7, 23, 31], [1, 4], [1, 18, 21], [1, 6, 21], [1, 10], [1, 6, 18], [1, 5, 10, 21], [1, 6, 18, 22], [1, 6, 10, 21], [1, 6, 10], [1, 6, 10], [2, 10, 12], [4, 8, 34], [4, 8, 35], [4, 8, 35], [0, 8, 35], [1, 23, 35, 38], [4, 8, 35], [4, 8, 35], [4, 8, 35], [2, 40], [2, 33, 40], [2, 4, 18], [0, 6, 21], [1, 10, 18, 21], [1, 10, 18, 21], [2, 21], [1, 6, 10], [1, 6, 9], [1, 10], [1, 5, 10], [2, 6, 10], [1, 6], [0, 6, 21], [4, 18, 30, 34], [4, 35, 38], [23, 35, 38], [23, 35, 38], [4, 8, 35, 38], [4, 8, 35], [4, 8, 35], [23, 31, 35, 38], [2, 7, 23, 35], [3, 23], [0, 18, 21], [1, 6, 10, 21], [1, 10, 18, 21], [18, 20], [18, 21], [0, 3, 6, 21], [0, 7, 21], [0, 3, 7, 21], [3, 6], [2], [3, 6], [4, 26, 29, 33], [4, 8, 19], [16, 19, 29, 31], [4, 8, 19, 31], [4, 8, 11], [4, 8, 21], [4, 8, 21, 33], [4, 21, 30, 33], [2, 7], [2, 4, 19], [1, 4], [1, 6, 11, 18], [1, 6, 15, 18], [1, 10, 15, 18], [0, 6, 18], [1, 5, 10, 18], [5, 10, 18, 23], [1, 10, 15, 18], [0, 6, 9, 18], [2, 7], [4, 7], [0, 3, 8], [4, 8, 11], [4, 8], [0, 4, 8], [0, 4, 9], [4], [4, 8], [4, 8], [4, 8, 11], [0, 4, 11], [1, 4, 8, 16], [3, 8, 43], [4, 8, 31, 43], [4, 8, 31, 43], [4, 7, 31], [2, 6, 28, 31], [2, 16, 31], [2, 6, 11], [2, 7, 28], [0, 6, 23], [6, 23], [2, 11, 23], [0, 11, 23, 38], [0, 7, 11, 38], [23, 31, 38], [2, 6, 12, 38], [0, 4, 7, 12], [0, 7, 12, 38], [0, 38], [1, 12, 38], [2, 7, 12, 38], [2, 7, 12, 38], [2, 5, 12, 38], [0, 7], [0, 43], [1, 24], [0, 7, 12], [0, 7, 12], [0, 7, 12], [0, 3, 12], [0, 3, 9, 12], [2, 9, 11, 43], [2, 9, 11, 43], [4], [4, 9, 11, 21], [4, 42], [0, 4, 23], [4, 11], [4, 11], [4, 11, 40], [4, 11, 40], [3, 6, 11], [11, 16, 32, 42], [0], [0, 3, 8, 31], [4, 8, 31], [4, 8, 31], [1, 4, 31, 43], [0, 6], [0, 6, 16, 32], [6, 40], [1, 7], [4, 6], [4, 7], [4, 9, 11], [2, 9, 11, 38], [9, 11, 26, 38], [1, 9, 38], [0, 3, 6, 33], [2, 5, 9], [2, 9, 33], [2, 40], [2, 9, 38, 43], [4, 9, 38, 43], [4, 9, 38, 43], [2, 5, 19], [9, 19, 38], [1, 9, 14], [4, 14], [2, 9, 19, 31], [9, 14, 31], [2, 9, 14, 19], [1, 38], [0, 14, 21, 33], [0, 7, 21, 43], [12, 21], [6, 12], [6, 12, 21], [0, 11, 23], [1, 4, 11], [3, 6, 11], [1, 23, 30], [2, 11], [0, 6, 11], [0, 11], [0, 23], [4, 6, 11, 23], [2, 6, 11], [6, 12, 27], [14, 27, 39], [0, 15, 27], [5, 15, 27, 39], [6, 15, 39], [6, 16, 39, 42], [0, 3, 42], [1, 16, 42], [2, 16], [1, 6, 11], [6, 11], [30], [6, 16], [6, 11, 16], [2, 6, 11, 16], [0, 15, 35], [2, 5, 15], [1, 10, 15], [1, 15], [15, 30, 35, 37], [0, 6, 30, 37], [6, 11, 37], [16], [16], [5, 16], [6, 16], [6, 15, 35], [2, 5, 35], [15, 34], [0, 3, 5], [2, 11, 16], [4, 16], [4, 8, 16], [4, 8, 40], [4, 8, 40], [4, 8, 40], [4, 11, 40], [4, 11], [0, 3, 9, 16], [4, 8, 16], [4, 8, 16], [1, 4, 16], [4, 8, 11, 16], [4, 8, 16], [4, 8], [4, 8, 11, 40], [2, 7, 16], [2, 7, 16], [2, 7, 16], [6, 16, 35], [6, 16, 36], [6, 16, 36], [0, 7, 16, 38], [0, 7, 16], [0, 7, 16, 38], [0, 7, 22, 38], [0, 7, 23, 38], [0, 7, 23, 38], [0, 7, 38], [0, 7, 23, 36], [0, 3, 7, 36], [0, 7, 23], [0, 4, 7, 43], [0, 7, 23, 43], [0, 7, 21, 33], [0, 7, 21, 31], [0, 7, 21, 31], [0, 7, 43], [0, 7, 33], [0, 7, 23, 33], [2, 5, 9, 23], [2, 9], [2, 5, 9, 23], [0, 4], [4], [4, 11], [4, 11], [4, 11, 31], [4, 11, 43], [4, 11, 31], [4, 11, 43], [0, 3, 6], [6, 11, 24, 42], [0, 4], [4, 8, 40], [4, 8, 40], [4, 40], [4, 8, 24, 40], [4, 8, 24, 40], [0, 6, 23], [6, 23], [23], [4, 21, 23], [4, 21], [5, 23, 31, 38], [2, 9, 19, 31], [2, 9, 31, 38], [0, 9, 30, 38], [0, 3, 9, 38], [2, 9, 16, 38], [2, 9, 38], [0, 9, 16, 38], [2, 9, 40], [2, 9, 16, 21], [0, 8, 16], [2, 5, 9, 16], [2, 9, 16, 18], [2, 9, 16, 38], [2, 9, 16, 31], [5, 15, 31, 38], [2, 9, 19], [2, 9], [1, 9], [0, 4, 12, 21], [2, 7, 12, 21], [1, 7, 11], [6, 11], [6, 12, 23], [12, 42], [12, 23], [6, 11, 23], [6, 12, 23], [11, 18], [0, 6, 11, 18], [3, 5, 11], [11, 23], [0, 6, 23, 39], [6, 27, 39], [0, 6, 27, 39], [2, 6, 11, 23], [6, 11, 23], [6, 11], [1, 6, 27, 39], [1, 9, 11, 39], [9, 21, 39], [6, 11], [2, 6, 11], [0, 6, 11], [0, 6, 11], [6, 11], [0, 6, 23, 35], [0, 6, 11], [2, 6, 11, 35], [6, 11, 35], [0, 6, 11], [2, 9, 35], [6, 11], [6, 11], [3, 12], [14], [2, 15], [15], [4, 6, 15], [6, 11, 16], [3, 6, 11, 16], [6, 11, 16], [2], [2, 6, 35], [0, 8, 18, 30], [4, 8, 30, 34], [4, 8, 31, 33], [4, 8, 35, 38], [4, 19, 35, 38], [4, 23, 35, 38], [4, 23, 31, 35], [4, 8, 23, 35], [4, 35, 38, 42], [2, 19, 23, 31], [2, 7, 11, 19], [1, 21], [1, 6, 18, 21], [1, 6, 10, 21], [1, 6, 10, 21], [1, 6, 10, 21], [1, 6, 14], [1, 6, 10], [1, 10], [0, 3, 7], [2, 7], [0, 7, 16, 20], [4, 12, 18, 22], [4, 8, 19, 23], [4, 19, 23], [2, 19, 23], [4, 8, 19, 23], [4, 8, 14, 23], [4, 8, 19, 23], [2, 19, 21, 23], [2, 4, 7, 21], [2, 18, 21], [1, 6], [1, 10, 18, 22], [1, 10, 18], [1, 6, 18, 21], [1, 6, 21], [1, 6, 10], [1, 6, 10], [1, 10], [0, 3, 6, 9], [0, 7], [3, 7], [0, 4, 29, 34], [4, 8, 36], [4, 23, 31, 35], [4, 8, 23, 35], [23, 31, 35, 38], [4, 8, 35, 38], [4, 19, 23, 35], [1, 4, 31, 35], [1, 23, 31, 35], [2, 19, 23, 31], [2, 7], [0, 6, 21], [1, 10, 18, 20], [1, 10, 18, 21], [1, 18, 21], [1, 6, 10, 21], [6, 10, 21], [0, 3, 10], [1, 6, 10], [0, 7], [2, 7], [0, 7, 18, 30], [4, 8, 19, 31], [4, 8, 19, 28], [1, 4, 19, 31], [0, 4, 8], [0, 4, 8], [4, 29, 31, 32], [4, 8, 30, 33], [1, 7, 33, 40], [2, 7, 11, 19], [2, 7, 28], [0, 6, 15, 18], [1, 11, 15, 18], [0, 10, 15, 18], [1, 18], [1, 6, 15, 18], [1, 6, 15, 18], [1, 6, 15, 18], [1, 6, 10, 15], [0, 3, 7], [1, 7], [7], [7], [7, 35], [4, 7, 11, 35], [4, 7, 11], [4, 11], [4], [4], [4], [4], [4, 30], [], [6], [6], [6, 11], [11], [6, 11], [11], [11], [], [40], [28, 40], [4], [9], [4, 9], [4, 9], [4, 9], [4, 9], [4, 9], [9], [9], [9, 16], [9], [9], [9], [9], [9, 31, 43], [30], [9, 19], [9, 19, 23], [9, 19, 23], [7], [19, 38], [4, 6], [7], [0, 7], [0, 7], [7], [7], [7, 12], [0, 7], [0, 7], [0], [0], [2, 5, 16], [2, 6, 9, 16], [2, 9, 16], [2, 9], [2, 9], [2, 9, 14], [2, 9, 23], [2, 9, 23], [9, 14, 23], [23], [40], [7, 11, 16, 23], [4, 7, 11], [4, 23], [4, 11], [4], [4, 23], [4, 23], [4], [4], [2, 16], [6, 16], [6, 11, 15], [6, 11, 15], [6, 11, 15], [6, 11, 16], [6, 16], [6, 11], [6, 11], [6, 11], [6, 11, 21], [4, 11, 21], [4, 7], [4, 7, 19], [4, 19, 38], [4, 7], [2, 7, 19], [19], [11, 40], [7, 40], [14, 21], [9], [6], [11, 23], [6, 11, 23], [11, 23], [40], [1, 6, 10, 40], [1, 10, 30], [1, 6, 10, 30], [6, 10], [31, 43], [43], [9, 31], [9, 31], [4, 9], [9], [9], [4, 9], [4, 7], [4, 9], [4], [4, 8], [4, 8], [9, 16], [16], [9, 16], [4, 7], [4, 9], [4, 9], [9, 35], [35], [19], [6, 19], [1, 7, 19], [0, 7], [7, 16], [7, 16], [0, 7], [2, 7, 38], [2, 38], [7, 12, 38], [2, 7, 38], [4, 7, 38], [4], [2, 6], [2, 6, 9, 38], [2, 9, 38], [2, 38], [2, 6, 9, 38], [9, 37], [6, 9, 14], [2, 4, 9, 14], [2, 6, 9, 35], [2, 9, 35], [2, 4, 35], [4, 7, 16], [4, 7, 35], [4, 7, 11], [3], [4, 7, 11], [4, 11], [4, 11], [4, 7], [4], [4], [4], [6, 11], [6, 11], [6, 11], [6], [11, 15], [6, 11], [6, 11], [6, 11], [6, 11, 15], [6, 11, 15, 35], [11, 34], [4, 8, 11, 33], [4, 8, 35], [0, 4], [0, 4, 7, 35], [23, 31, 35, 38], [4, 8, 23, 35], [4, 8, 23, 35], [2, 7, 23, 35], [2, 7, 23, 35], [2, 19, 22, 31], [1, 6, 10, 18], [1, 6, 10, 21], [1, 10, 22], [3, 10, 21], [3, 6, 10, 21], [1, 6, 10, 21], [1, 6, 9], [1, 6, 10], [0, 4, 7], [0, 3, 7], [0, 4, 21, 33], [4, 8, 21, 34], [4, 8, 35], [4, 8, 23, 35], [2, 8, 23, 35], [4, 8, 18], [4, 8, 24], [4, 8, 36, 43], [4, 24, 33, 36], [2, 4, 23, 35], [2, 7, 11, 23], [2, 6, 9], [1, 6, 18, 21], [1, 10, 18, 21], [1, 6, 10, 21], [2, 4, 9, 21], [1, 6, 10, 21], [1, 10], [1, 6, 10], [0, 3, 5, 9], [0, 7], [0, 6, 12], [4, 8, 20, 22], [4, 7, 42], [4, 8, 19, 23], [0, 19, 23, 35], [0, 4, 8, 23], [4, 8, 23], [4, 7, 23], [4, 23], [4, 7, 19, 23], [2, 7, 19, 23], [2, 5], [1, 6, 10, 33], [1, 6, 33, 40], [1, 10, 33, 39], [0, 21, 32, 39], [1, 10, 33], [1, 10, 21, 33], [0, 6, 10, 21], [1, 6, 10], [0, 4, 7], [0, 3, 7], [4, 8, 14, 19], [4, 8, 18, 29], [4, 19, 28, 31], [19, 28, 31], [0, 4, 8], [4, 14, 18], [4, 19, 31, 33], [4, 21, 30, 33], [1, 4, 7, 30], [2, 7, 11, 19], [2], [0, 6, 9, 18], [1, 6, 15, 18], [2, 10, 18], [0, 6, 18], [0, 3, 6, 10], [1, 4, 10, 18], [1, 4, 10, 18], [0, 5, 9, 18], [0, 3, 7], [0, 7], [2, 7], [4, 39], [4, 8, 11, 40], [4, 8, 40], [4, 8, 40], [4, 8, 11, 40], [4], [4, 11], [2, 40], [2, 7, 43], [2, 7, 40], [3, 7, 40], [0, 7, 40], [0, 7, 40], [0, 7, 41], [4, 7, 40, 42], [4, 6, 40, 42], [0, 7, 40, 42], [0, 7, 40, 42], [0, 4, 6, 40], [0, 4, 35, 42], [0, 7, 40], [4, 9, 35, 42], [2, 9, 35, 42], [9], [9], [4, 9, 31, 38], [2, 9, 39, 42], [2, 9], [1, 9, 31, 38], [1, 4, 9, 38], [2, 9], [0, 4], [4, 11], [4, 42], [0, 4, 11, 40], [4, 11, 40], [4, 11, 40], [4, 40], [4, 11, 40], [4, 7, 38, 40], [3, 6, 40], [4, 6, 40], [0, 4, 38, 40], [4, 8, 40], [4, 8, 40, 43], [4, 8, 38, 40], [4, 8, 40], [4, 8], [4, 8, 11, 40], [4, 8, 42], [4, 7, 40, 43], [2, 7, 40, 43], [2, 7, 40, 43], [0, 4, 7, 42], [0, 3, 7], [0, 7, 43], [0, 7, 42], [0, 4, 7, 40], [0, 7, 40], [0, 7, 40], [1, 7, 38, 40], [0, 4, 38, 43], [2, 11], [2, 7, 42], [4, 6, 9, 31], [9, 31, 35], [2, 9, 31], [9, 31, 35, 43], [2, 6, 9], [2, 9], [2, 9, 43], [2, 9, 34, 42], [4, 9], [2, 9, 38], [0, 7, 35, 38], [4, 11], [4, 19, 38], [6, 11], [6, 11, 35], [4], [4, 8, 42], [1, 4, 8, 11], [4, 8], [0, 4, 8], [4], [4, 6], [4, 8], [4, 8, 11], [4, 11, 38, 40], [4, 7], [4, 8, 11], [4, 8, 11], [4, 8, 11, 40], [4, 7, 11, 40], [2, 7, 11, 40], [2, 7], [0, 4, 7], [0, 7, 43], [7, 16, 40], [0, 7, 40], [0, 3, 7, 43], [3, 7, 28, 40], [7, 11, 40, 43], [2, 7, 28, 40], [0, 3, 7, 40], [0, 7, 40], [0, 5, 9, 38], [2, 9], [2, 9, 38], [2, 9, 40], [4, 9], [4, 9, 38], [2, 9], [2, 9, 38, 40], [4, 7, 9, 33], [4, 9, 30], [2, 9, 38], [0, 4], [4, 11, 40], [4, 7, 38, 40], [6, 11, 38, 40], [2, 5, 11, 31], [0, 4, 40], [0, 4, 8, 43], [0, 4, 40], [0, 5, 8, 34], [1, 4, 8, 38], [4, 38, 40, 43], [4, 8, 11, 38], [4, 8, 43], [4, 8, 38, 40], [4, 8, 31, 38], [4, 8, 38, 43], [2, 7, 38], [2, 7, 11, 40], [2, 7, 28], [4, 6], [6, 38, 40, 43], [1, 4, 7, 36], [0, 4, 7], [0, 7, 38, 43], [0, 7, 40, 43], [0, 7, 36], [0, 7], [0, 7, 40], [0, 7, 38, 40], [0, 5, 36], [2, 7, 11, 40], [2, 7, 38, 40], [4, 9, 38], [2, 9, 42], [2, 9, 42], [2, 9], [4, 9], [1, 9, 38], [2, 9, 38], [2, 9, 35, 38], [4, 9], [2, 9, 28], [4, 28], [4, 11, 40], [4, 11], [4, 11], [6, 11, 16, 35], [4, 11], [0, 4, 8, 35], [0, 4, 8, 38], [4, 8], [0, 4, 11], [11, 34], [0, 4, 7], [4, 8, 11], [4, 8, 11, 31], [4, 8, 31], [4, 8], [0, 4, 7, 31], [2, 7, 11], [2, 11, 40], [2, 7, 31, 42], [6, 31], [6, 11, 42], [0, 4, 7, 34], [0, 7, 16, 23], [0, 7, 16, 23], [0, 7, 23], [0, 4, 7, 23], [2, 23], [2, 7, 11, 23], [2, 7, 23], [4, 7], [0, 7, 43], [0, 7, 23, 43], [2, 9], [2, 9, 30], [2, 6, 9], [2, 9, 43], [0, 3, 9, 40], [2, 9, 38], [2, 9, 43], [2, 9, 43], [2, 9, 38, 43], [2, 9, 38], [0, 3, 5, 11], [4, 11, 31], [4, 11], [3, 11, 43], [4, 11, 33, 38], [0], [4, 8], [1, 4, 38, 43], [4, 7], [0, 4, 8], [4, 8], [4], [4, 8, 42], [4, 8], [4, 11], [4, 6, 11], [4, 38, 40], [2, 7, 28], [2, 11, 40], [4, 38, 40], [0, 6, 29, 40], [0, 38, 40], [0, 7, 40, 43], [0, 7, 42, 43], [1, 7, 43], [0, 7, 43], [0, 6, 31, 43], [2, 7, 11, 43], [2, 7, 11, 43], [2, 7, 11, 43], [0, 4, 7, 43], [0, 7, 40], [2, 4, 9, 31], [2, 9, 31, 43], [2, 9, 31], [2, 9, 31], [4, 6, 9, 43], [2, 9, 31, 35], [2, 9, 35], [2, 9, 35], [2, 5, 9, 35], [2, 9, 34], [4, 10, 33], [4, 6, 11], [4, 11], [0, 4], [33, 35], [4, 11, 31], [0, 4, 35, 43], [0, 4, 8], [4, 8], [4, 7, 31], [4, 8], [4], [4, 8, 11], [4, 8], [0, 4, 8, 30], [4, 8, 11], [4, 11, 26, 38], [1, 4, 7], [2, 7], [2, 7], [4, 7, 11, 40], [6, 38, 40], [3, 6, 38], [0, 4, 7], [0, 7, 16], [0, 7, 16, 30], [0, 4, 7, 38], [2, 9, 42], [2, 7, 11, 43], [2, 7, 11, 43], [2, 6], [0, 7], [0, 7], [2, 6, 9], [2, 9, 31], [2, 9, 38], [2, 9, 31], [5, 9, 31], [2, 9, 31], [2, 9, 42], [2, 9, 38], [4, 9, 30, 38], [0, 9, 30], [4, 43], [4, 11, 31], [4, 11, 30, 40], [0, 4, 11], [6, 11, 28, 40], [4, 11, 40], [4, 8], [4, 8, 11], [0, 4], [0, 4, 8, 23], [4, 8, 23, 38], [0, 4, 7, 38], [4, 8, 39, 40], [4, 8, 11, 40], [1, 4, 8, 40], [4, 8, 11, 40], [4, 8, 40], [2, 40], [2, 4, 7, 39], [2, 4, 11, 38], [6, 11, 38], [6, 11, 30], [0, 4, 7], [0, 3, 7], [0, 7, 43], [0, 7], [0, 4, 7, 9], [2, 7], [2, 7, 11, 40], [2, 7, 11], [0, 3, 33, 38], [0, 3, 7, 40], [1, 4, 6, 38], [2, 9, 35, 38], [2, 9, 38, 43], [0, 9, 35, 38], [9, 31, 35, 38], [2, 9, 38], [2, 9, 30], [2, 9, 38], [0, 6, 9, 43], [2, 9, 38], [4, 9, 40], [3, 6, 11], [4, 11, 40], [4, 11, 40], [0, 4, 11], [0, 3, 6, 11], [3, 28], [4, 8, 43], [4, 8, 11, 43], [0, 3, 8], [4, 8, 42], [4, 8], [4, 7, 33, 40], [4, 8, 40], [4, 8, 35, 38], [4, 8, 23, 35], [4, 35, 38, 40], [4, 8, 35, 40], [4, 8, 21, 33], [4, 19, 33, 40], [2, 4, 7, 21], [2, 11, 40], [2, 6, 40], [1, 6, 22, 34], [1, 10, 33, 40], [1, 10, 33, 40], [0, 9, 32, 39], [1, 4, 10, 40], [1, 10, 33, 40], [1, 10, 33, 40], [1, 6, 10, 40], [0, 4, 7, 40], [0, 7, 40], [4, 8, 12, 21], [4, 8, 35, 40], [4, 8, 23, 35], [14, 23, 31, 35], [1, 4, 7, 40], [0, 4, 8, 11], [4, 8, 35], [4, 24, 33, 36], [7, 24, 33, 36], [2, 11, 19, 23], [2, 7, 40], [0, 4, 18, 21], [1, 6, 10, 21], [1, 6, 10], [1, 10, 42], [1, 6, 10], [1, 10], [1, 6, 10, 42], [1, 6, 10, 42], [0, 3, 6, 43], [0, 7, 11, 43], [7, 11, 16, 20], [4, 8, 29, 33], [4, 8, 35, 40], [14, 23, 31, 35], [4, 19, 31, 35], [4, 8, 31, 35], [4, 8, 23, 35], [4, 8, 35, 40], [4, 7, 23], [2, 7, 23, 40], [2, 7, 40], [2, 5, 39], [1, 10, 30, 33], [1, 6, 10, 32], [1, 10, 39], [1, 4, 33, 39], [0, 7, 21, 33], [0, 7, 39], [0, 3, 7, 39], [0, 3, 7], [2, 6, 39], [0, 6], [4, 19, 27, 40], [4, 8, 19, 40], [4, 19, 28], [3], [0, 3, 6, 8], [4, 8, 11, 19], [4, 8, 30, 33], [4, 30, 33], [2, 4, 7, 11], [2, 7, 11, 40], [1, 5, 10], [0, 6, 10, 18], [1, 10, 15, 18], [0, 15, 18], [0, 7, 15, 18], [4, 7, 18], [0, 15, 18], [6], [2, 6, 42], [1, 6, 10], [1, 6, 10], [0, 3, 8, 23], [4, 8, 40, 41], [4, 8, 35, 42], [3, 28, 35, 42], [8, 28, 35, 42], [4, 8, 35, 42], [4, 8, 42], [4, 8, 35, 42], [4, 7, 35, 42], [2, 7, 35, 42], [2, 11, 28, 35], [0, 35, 40, 42], [2, 35, 38, 42], [0, 8, 35, 42], [0, 7, 35, 40], [0, 4, 7, 35], [0, 7, 35, 40], [0, 7, 35, 42], [31, 35, 38, 42], [2, 7, 11, 40], [2, 7, 31, 40], [2, 31, 38], [2, 9, 32, 39], [2, 9, 32], [0, 9, 33, 40], [9, 33], [2, 9, 33], [2, 9, 33], [2, 9], [2, 9], [2, 9, 33], [0, 3, 33, 40], [4, 11, 33, 40], [4, 7, 11, 31], [4, 11], [6, 32, 35, 40], [6, 31, 35], [4, 31, 35], [4, 8, 11, 43], [4, 8, 11, 43], [4, 8, 40, 42], [3, 28, 35, 40], [4, 8, 11], [0, 4, 8], [4, 8], [4, 8, 11, 33], [0, 4, 8, 35], [4, 8, 17, 35], [4, 25, 33, 42], [8, 34, 35, 41], [8, 23, 35, 42], [1, 6, 35, 43], [2, 7, 35, 42], [2, 7, 26, 33], [0, 7, 33, 43], [0, 7, 33, 40], [0, 7, 33, 38], [0, 7, 33, 38], [0, 4, 33, 38], [0, 7, 33, 38], [2, 7, 11, 38], [2, 7, 38, 40], [0, 7, 33, 40], [0, 7, 33], [2, 9, 35, 42], [2, 9, 38], [4, 6, 9, 38], [2, 33, 35, 40], [0, 9, 38], [1, 6, 9, 38], [4, 33, 38, 40], [4, 9, 33, 40], [9, 33, 40], [2, 9, 38], [3, 28, 35, 38], [4, 11, 19, 38], [4, 11, 37, 42], [4, 6, 30, 38], [0, 6, 35], [2, 6, 35], [1, 4, 33], [4, 8, 11, 31], [4, 8, 31], [4, 11, 31], [4, 11, 31, 38], [4, 19, 38, 43], [0, 4, 8, 35], [4, 8, 11], [4, 8, 11], [4, 8, 11], [0, 4, 8, 11], [1, 27, 28, 35], [2, 11, 28, 35], [2, 7, 35, 40], [6, 28, 35, 40], [6, 28, 40, 43], [0, 7, 30, 40], [0, 37, 40], [0, 7, 40], [0, 7, 39, 40], [0, 7, 40], [0, 40], [2, 7, 28, 40], [2, 7, 11, 28], [2, 7], [0, 7, 30], [0, 7, 40, 43], [2, 9, 31, 38], [2, 9, 30, 38], [2, 9, 38], [2, 9, 38], [2, 9, 38], [2, 4, 9, 38], [4, 9, 26, 38], [4, 9, 26, 38], [4, 9, 26, 38], [4, 9], [4, 33], [4, 11, 33, 35], [4, 11], [4, 6, 35], [0, 6, 11, 33], [11, 33, 35], [8, 31], [4, 8, 11, 31], [0, 4, 31, 38], [4, 11, 31], [4, 11, 31, 38], [0, 4, 7, 43], [4, 8, 31, 35], [4, 8, 31], [4, 31, 38, 42], [4, 8, 11, 31], [4, 11, 31, 38], [2, 7, 31, 38], [2, 31, 33, 38], [30, 31, 37, 40], [6, 30, 33, 37], [11, 31, 33, 40], [7, 33], [0, 7, 30, 33], [7, 33, 40], [7, 33, 40], [3, 7, 33, 40], [2, 7, 33, 43], [2, 7, 31, 33], [2, 7, 33, 40], [0, 7, 33, 36], [0, 7, 31, 36], [0, 7, 31], [2, 9, 31], [2, 9, 33], [2, 9, 33, 38], [2, 9, 33, 38], [2, 6, 31, 33], [2, 9, 18, 37], [4, 31, 36, 37], [4, 30, 37, 38], [4, 9, 30, 38], [1, 9, 30, 37], [4, 6, 35, 40], [4, 35], [4, 35], [4, 35, 42], [4, 28, 35], [4, 35], [4, 35], [4, 35], [3, 28, 35], [2, 28, 35], [6], [0, 3], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [0, 18, 30, 33], [19, 29, 31, 36], [19, 28, 31, 35], [19, 28, 31, 35], [16, 28, 31, 35], [19, 31], [28, 30], [18, 28, 30, 35], [16, 28, 30, 35], [7, 18, 28, 30], [18, 30, 33], [7, 30], [7, 18], [14, 26], [14, 19], [19, 26], [19, 26], [20], [11, 19, 26, 31], [11, 14, 19, 26], [11, 19], [12, 19, 31], [12, 19], [9, 12, 19, 26], [9, 12, 19, 26], [4, 12, 19], [12, 19], [12, 19], [12, 21], [11, 20], [12, 19, 24], [12, 19], [11, 20], [12, 18], [11, 18, 23], [11, 18], [11, 18, 23], [10, 18], [19, 23], [11, 19], [18], [9, 18], [9], [17], [19, 31, 35], [19, 31, 35, 42], [19, 35, 42], [19, 35, 38, 43], [31, 35], [19, 21, 36, 43], [33, 36, 40, 43], [21, 36, 43], [11, 22], [], [18, 30, 34], [30, 36, 43], [6, 30, 36, 43], [30, 36, 43], [30, 36], [18, 30, 36, 43], [18, 30, 36, 43], [7, 16, 30], [6, 16], [6, 16, 28], [4, 28, 35], [4, 28, 35], [28, 35], [16, 28, 35], [16, 28, 35], [16, 28, 35], [28, 35], [11, 23], [7, 18, 20, 21], [19, 21, 33, 40], [7, 19], [6, 19], [6, 21], [6, 21, 33, 37], [6, 22], [6, 22, 33, 40], [6, 21, 30, 40], [6, 21, 30], [6], [6, 9], [6, 14, 19, 22], [6, 19, 23, 33], [31, 33, 35, 38], [31, 33, 35, 42], [], [], [14], [6, 14, 16, 19], [6, 14, 16, 26], [6, 19, 26, 28], [16, 19, 26, 31], [7], [7, 24, 28, 33], [28, 30, 33, 37], [30, 33, 37, 40], [4, 28, 30, 40], [30], [14, 21], [21, 24, 28, 33], [21, 24, 28, 33], [21, 24, 28, 33], [25, 28], [7, 24], [7, 15, 19], [18, 23], [15, 19, 22, 24], [6, 15, 23, 30], [], [19, 27, 31, 33], [15, 21, 28, 33], [7, 21, 28, 33], [7, 27], [9, 19], [19, 22, 27, 31], [19, 31, 35, 40], [30, 31, 35, 40], [31, 35, 38, 40], [23, 31, 35, 40], [31, 32, 35, 40], [11, 31, 35, 40], [11, 28, 30, 35], [11], [7, 11], [11], [12, 35, 37, 41], [18, 30, 33, 42], [18, 30, 33, 38], [6, 12, 38, 42], [12, 26, 33, 42], [12, 30, 38, 42], [12, 18, 30, 42], [12, 18, 33], [12, 24, 31, 36], [12, 16, 28, 31], [12, 15], [11, 15, 18], [11, 15, 18, 27], [11, 23, 27, 34], [18, 23, 27, 34], [18, 23, 28], [18, 23, 26, 33], [18, 23, 27], [15, 18, 23, 30], [23, 26, 30, 33], [15, 23, 30], [11, 18, 23, 30], [11, 18, 23], [11, 23], [6, 11], [], [], [11, 15, 27], [11, 15], [15], [15, 23], [11], [3, 11], [3], [3], [], [], [6], [11], [11], [11], [11], [11], [11, 25], [], [], [25], [], [], [], [], [30], [3, 30], [4], [4, 7, 26, 29], [19, 28, 31], [19, 28, 31], [7, 28, 31, 35], [4, 7], [4, 21, 30, 38], [30, 35, 37, 40], [30, 35, 37, 40], [4, 7, 35, 40], [28, 30, 35, 40], [7, 18, 30, 40], [7, 30, 33, 37], [7, 16, 42], [14, 16, 28], [16, 26, 28, 35], [15, 26, 33], [11, 26, 29, 33], [26, 31, 33], [11, 19, 31, 33], [11, 19, 31, 33], [11, 31, 33], [9, 19, 24, 31], [19, 31, 33], [19, 31], [19, 26, 31], [12], [10, 12, 19], [12, 21], [12, 19, 21, 33], [12, 19], [12, 19, 31, 38], [18], [11, 18, 30, 37], [11, 18, 37], [11, 18], [11, 18, 23], [18, 23, 30, 37], [18, 23, 30, 37], [11, 18, 37], [11, 18, 30, 37], [9, 11, 30], [9, 11], [11, 19, 22, 34], [19, 23, 35], [19, 23, 31, 35], [11, 19, 31, 35], [11, 19, 23, 35], [19, 23, 31], [11, 19, 23, 35], [11, 21, 33, 36], [21, 24, 33, 36], [11, 21, 33], [11, 25], [24], [18, 24], [18, 24, 36], [6, 23], [9, 19, 23], [19, 23], [18, 23], [7, 18, 23], [6, 16, 28], [6, 16, 28], [6], [4, 17, 19, 29], [19, 23, 31, 35], [4, 19, 31, 35], [4, 19, 31, 35], [19, 31, 35], [11, 19, 31, 33], [19, 31, 33, 38], [11, 19, 33, 40], [7, 21, 33, 40], [19, 31], [6], [6, 21, 40], [6, 21, 31], [6, 21], [6, 18], [6], [6], [6], [6], [6, 12, 15], [6, 14, 19, 23], [31, 33, 36, 42], [31, 33, 35, 42], [19, 23, 26, 35], [7, 35], [7, 19], [7, 14, 16], [6, 14, 20], [6, 16, 28], [16, 19, 26], [16], [6, 21, 33], [30, 31, 33, 40], [30, 31, 33, 40], [21, 30, 33, 40], [30, 43], [24, 33], [19, 24, 27, 30], [24, 28, 33, 36], [24, 28, 33], [24, 28, 36], [19, 36], [7, 15, 18, 22], [18, 23], [23], [6, 15, 18, 23], [9, 18, 23], [20, 21, 32], [21, 33], [7, 21, 33], [7, 33], [15, 22, 33], [15, 19, 23, 33], [23, 31, 35], [35, 38, 40, 43], [11, 31, 35, 40], [23, 31, 35, 40], [23, 31, 40], [11, 31, 40], [11, 16, 28], [], [11], [11], [11, 26], [25, 33, 35, 42], [26, 30, 33, 42], [12, 26, 33, 42], [12, 26, 33, 42], [26, 30, 33, 42], [12, 26, 33, 42], [12, 26, 33, 42], [12, 24], [12, 24, 28, 35], [12, 16, 28], [12, 15, 23], [15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [11, 15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [11, 18, 23, 35], [11, 18, 35], [15, 18, 23, 35], [11, 23, 35], [15, 23, 35], [], [21], [11, 15], [15], [11, 15], [15], [3], [3, 15], [15], [], [], [30], [2], [], [], [], [], [], [], [21], [], [], [], [1], [5, 8], [8], [5], [26, 29], [4, 6, 18, 29], [4, 18, 28], [4, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [11, 19, 27], [2, 6, 19, 26], [12, 26, 31], [0, 12, 27, 31], [12, 26, 30, 31], [7, 12, 18, 27], [0, 26, 30, 38], [12, 28, 30, 37], [12, 27], [26, 29, 36, 38], [2, 26, 30, 38], [18, 26, 30, 37], [26, 30, 37, 38], [9, 18, 26, 30], [2, 6, 18, 26], [2, 18, 26, 30], [2, 14, 26], [4, 23], [4, 22, 23], [16, 23], [4, 16, 23, 28], [4, 16, 38], [0, 4], [4, 7, 11], [10, 16], [18, 27], [20, 29], [16, 19, 28], [16, 19, 28], [4, 16, 28, 30], [16, 19, 31], [4, 16, 30], [11, 16, 28, 30], [0, 4, 18, 30], [19], [7, 16, 19, 31], [0, 7, 19, 31], [7, 16, 31], [2, 16, 18, 28], [0, 16, 18, 28], [7, 16, 27, 28], [2, 26, 29, 38], [18, 26, 30, 38], [18, 26, 30, 38], [2, 26, 30, 37], [18, 26, 30, 38], [2, 18, 26, 30], [6, 18, 26, 30], [1, 18, 26, 30], [4, 11, 26], [4, 11, 28], [4, 11, 28], [4, 28], [28], [2, 7, 28], [4, 7, 28], [4, 31], [27, 30, 37], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28], [4, 16, 19, 28], [4, 27, 30, 38], [0, 7, 27], [0, 7, 27, 31], [0, 24, 26, 31], [0, 7, 18, 24], [18, 24], [18, 24], [11, 24], [16, 22, 29, 35], [9, 23, 30, 36], [23, 30, 35, 37], [18, 21, 30, 37], [18, 21, 30, 35], [0, 9, 18, 21], [9, 18, 21, 30], [9, 16, 20, 30], [4, 19, 38], [28, 32, 35, 39], [20, 32, 35, 39], [4, 19, 23, 28], [4], [4], [4, 11], [4], [4, 16, 19], [19], [4, 19], [4, 19, 23, 35], [4, 19, 23], [19, 28, 35], [4, 19, 28], [4, 16, 19, 28], [18, 19, 30], [0, 16, 19, 32], [16, 19, 30, 32], [0, 16, 19], [0, 16, 32], [16, 18, 28, 30], [0, 7, 16], [2, 16, 29], [16, 29], [9, 18, 30], [2, 9, 18, 30], [9, 30], [4, 18, 27], [18, 26], [18, 27, 30], [2, 9, 26], [4, 26, 39, 40], [28, 35], [4, 16, 28, 35], [27, 29, 35], [4, 28, 29, 35], [28, 29], [4, 28, 29], [4, 40], [4, 19, 39], [4, 31, 38, 40], [4, 28, 31, 40], [4, 31, 38, 40], [4, 28, 31], [4, 28, 31, 38], [4, 28, 31], [4, 7, 19, 40], [4, 6, 31, 39], [26, 31, 38], [26, 31, 35, 38], [19, 26, 31, 38], [25, 27, 31, 38], [27, 31, 38], [26, 31, 38], [26], [1, 25, 29, 30], [18, 27, 30], [26, 30, 37, 38], [18, 26, 30, 37], [2, 9, 26, 30], [2, 24, 30, 36], [2, 18, 30, 36], [2, 16, 23], [4, 25], [23, 35], [28, 35], [16, 35], [4, 28, 35], [4, 11, 16], [4, 7, 11], [7, 43], [16, 27, 30, 31], [16, 19, 28, 31], [16, 28, 31, 38], [28, 31], [16, 28], [16, 19, 28, 30], [16, 30], [4, 11, 16], [0, 4, 30, 31], [0, 16, 19, 31], [0, 31], [0, 16, 31], [16, 30, 31], [2, 28, 29], [16, 28, 30], [0, 16], [16, 26, 28], [2, 26, 30, 38], [9, 26, 30, 38], [2, 9, 26, 30], [2, 9, 30, 38], [2, 26, 30], [2, 6, 18, 30], [2, 6, 26, 30], [4, 11, 28, 39], [11, 19, 28, 35], [4, 19, 28, 35], [28, 35], [4, 28], [6, 19], [3, 7], [4], [19, 31, 38], [19, 28, 31, 38], [19, 28, 31, 38], [28, 31, 38, 40], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19], [4, 19, 26], [0, 4, 26, 31], [27, 31, 38], [7, 19, 26, 31], [27, 31, 38], [24], [0, 43], [7, 24, 31], [19, 31], [30, 33, 34, 40], [21, 23, 30], [9, 23, 30, 35], [18, 21, 23, 30], [2, 9, 21, 33], [2, 21, 22, 42], [19, 21], [2, 6, 9], [4, 16, 20], [4, 16, 19], [4, 19], [4, 16, 31, 43], [16, 23, 28, 31], [4], [4, 16, 23, 38], [4, 7], [4, 18, 19, 23], [4, 19, 23, 31], [4, 23, 31, 35], [4, 19, 23], [4, 23], [28, 31, 38, 40], [19, 31, 38, 40], [19, 28], [16, 19, 30, 31], [19, 31, 38], [30, 31, 38], [19, 30, 38], [0, 19, 28, 29], [16, 19, 28, 30], [0, 16, 19, 28], [0, 16, 27, 30], [30, 38], [2, 30], [2, 9, 30], [9, 18, 30], [6, 18, 30], [18, 26, 30, 38], [18], [2, 16, 30], [4], [16, 28], [4, 11, 16, 28], [0, 4, 11, 29], [0, 11, 16, 29], [4, 28, 33], [4, 7, 28], [11, 16, 19], [19, 28, 31, 34], [4, 28, 31, 35], [28, 31, 35, 38], [19, 28, 31, 35], [4, 19, 28, 31], [7, 19, 31, 34], [21, 31, 33, 40], [4, 21, 30, 33], [4, 18, 21, 30], [11, 19, 23, 31], [7, 19, 23, 31], [19, 23, 31, 38], [7, 23, 31, 38], [11, 23, 31, 38], [11, 31, 38], [11, 19, 23], [23, 31, 33, 35], [12, 24, 33, 36], [12, 24, 33, 36], [12, 24, 33, 40], [9, 12, 23, 33], [23, 31, 33, 35], [31, 33, 35], [11, 19, 21], [4, 6, 18, 21], [18, 21], [18, 21], [18, 21], [18, 23], [18, 33], [9, 18], [11, 15, 21], [1, 18, 22], [19, 23], [11, 19, 23], [19, 23, 31], [19, 23, 31, 35], [21, 23, 31, 42], [19, 23, 35, 38], [19, 23, 30, 33], [3, 9, 30, 33], [30, 33], [19, 28, 31, 35], [9, 28, 31, 35], [2, 7, 9], [7, 9, 15, 18], [6, 15, 18], [6, 15, 18, 27], [19, 21, 28, 30], [19, 21, 28, 31], [19, 21, 28, 30], [19, 28, 33, 35], [19, 28, 31, 33], [28, 30], [28, 31, 33, 38], [19, 21, 28, 31], [6, 26, 31], [6, 27, 30, 43], [6, 27, 30], [6, 27, 30], [6, 27, 30], [6], [6], [6, 16], [15, 18, 19], [16, 19], [6, 15, 31], [6, 15, 18, 27], [6, 14, 16, 31], [33, 34, 38, 43], [7, 28], [7, 13, 16, 19], [4, 9, 19, 33], [9, 21, 30, 33], [19, 30, 33, 40], [9, 17, 19, 28], [19, 28, 31, 33], [28, 31, 33, 35], [7, 29], [7, 17, 19], [17, 21, 29, 33], [18, 21, 30, 33], [6], [6, 16, 19], [6], [18, 21], [6, 18, 30, 33], [6, 18], [4, 11, 18, 21], [19, 23], [11, 19, 23], [11, 19, 23], [19, 23], [4, 6, 11, 19], [19, 23, 31], [11, 19], [4, 12, 20, 22], [21, 24, 31, 33], [12, 21, 24], [12, 21, 24, 33], [12, 21, 24, 33], [12, 21, 24, 33], [12, 21, 24, 33], [12, 24], [3, 24, 31], [23, 30, 35], [23, 30, 35], [2, 23, 30, 35], [18, 23, 30, 35], [23, 30, 35], [23, 30, 35], [23, 30, 35], [18, 23, 30, 35], [18, 23, 30, 35], [23, 30, 35], [23, 30, 35], [3, 23, 30, 35], [22, 30, 35], [18, 23, 30, 35], [3, 11, 30, 35], [4, 30, 35], [18, 22, 30, 34], [22, 30, 35, 37], [23, 30, 35], [30, 35], [30, 35], [9, 30, 35], [9], [1], [10, 33], [10], [10], [10], [10, 15], [10], [10], [4, 10], [10], [3, 5, 10], [3], [6], [0, 3, 7, 10], [3, 10], [2, 7, 11], [26, 31, 33], [4, 19, 31, 41], [19, 28, 31, 35], [19, 28, 31, 35], [7, 16, 28, 35], [4, 11, 30, 33], [18, 30, 33, 37], [1, 9, 22, 30], [4, 7, 18, 33], [20, 30, 35, 42], [20, 23], [19, 23], [19, 23], [11, 19, 35, 42], [20, 23], [11], [20, 32, 35, 38], [9, 21, 33, 36], [12, 21, 33, 36], [20, 33, 36, 40], [12, 19, 24], [19, 31, 35, 40], [11, 31, 35, 38], [11, 19, 28, 31], [3, 11, 18, 22], [11, 18, 21], [18, 21], [18, 21], [21], [21], [21], [11, 18], [7, 18, 22, 30], [7, 19, 23], [23, 31, 40, 42], [18, 23, 30, 35], [19, 23, 35, 42], [19, 23, 35, 42], [11, 23, 35, 42], [18, 25, 27, 31], [4, 9, 18, 33], [18, 21, 33], [9, 28, 32, 36], [28, 31, 35, 38], [19, 28], [2, 6, 14, 35], [6, 15, 18], [6, 15, 30], [19, 25, 27, 34], [28, 31], [28, 31], [19, 28, 31, 38], [27, 31, 38], [28, 31], [28, 31, 38], [16, 28, 31, 38], [3, 7, 27, 30], [6, 27, 30, 35], [6, 27, 30, 35], [6, 30, 35], [6, 30, 35], [6], [6, 10], [6], [2, 7, 20, 26], [7, 14, 19], [7, 11, 16, 19], [6, 15, 18, 35], [6, 15, 27, 42], [7, 19, 29, 31], [7, 28, 31, 35], [7, 18], [4, 9, 32, 39], [9, 30, 33, 37], [18, 30, 33, 37], [9], [4, 6, 30], [28, 31, 35, 38], [7, 19, 31, 38], [7, 19, 21, 28], [7, 18, 30, 33], [18, 21, 33, 40], [6, 16, 20], [6, 16, 19], [6, 28], [6, 22, 30], [21], [11, 16, 18, 19], [4, 18], [20], [20, 23, 28], [11, 20, 23], [23, 28, 35], [11], [23, 28], [12, 23], [12, 28, 33, 38], [30, 33, 36, 37], [12, 30, 33, 36], [30, 33, 36, 40], [30, 33, 36, 43], [30, 36, 37, 43], [12, 36], [6, 12], [4, 11, 25], [6, 11, 15], [11, 15], [11, 18, 27, 35], [11, 15, 27, 35], [11, 15, 35], [11, 15, 35], [11, 15, 27, 35], [11, 15, 27, 35], [11, 15, 35], [15, 23, 31, 35], [15, 23, 27, 35], [3, 15, 23, 35], [4, 15, 23, 35], [11, 15, 35], [3, 11, 15, 35], [3, 6, 11, 35], [11, 14, 27, 35], [2, 11, 15, 35], [23, 35], [15, 31], [23], [], [6, 11, 21], [5, 10], [3, 10], [3, 9], [3, 10], [10], [2, 9], [2, 10], [3, 10, 35], [3, 5], [3, 10, 12], [12], [9, 12], [6, 35], [6, 42], [4, 18], [4, 18], [4, 18], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [9], [11], [11], [11], [0], [], [], [7], [6, 9], [6, 9], [6, 9], [9], [11], [11], [11], [11], [], [11], [], [], [], [], [11], [0, 12], [0], [0], [0], [1], [0, 12], [23, 30], [23, 30], [11], [7], [6, 9], [6, 9], [6, 9, 16], [6, 9], [], [9], [], [], [], [], [], [9, 16], [10], [11, 18], [18, 23], [24], [11], [11], [11, 18], [11], [11], [7, 21], [6, 9], [6, 9], [6, 9], [7], [7], [6, 16], [6], [6], [6], [6], [6], [7], [7], [7, 23], [7, 16], [], [7, 16], [7, 16], [7, 16], [6], [6], [6], [], [6], [], [], [], [], [], [], [], [], [6], [7], [7, 19], [6], [6], [6], [7], [7], [7, 16], [], [6, 20, 27], [6, 9], [6, 9], [9], [8], [7], [7], [7], [7], [], [8, 21], [6, 9, 16], [6, 9, 21], [7, 11], [7, 11], [], [7, 11, 23], [0, 12, 16, 24], [16, 21], [], [], [11], [7, 11], [7, 11], [7], [], [], [], [], [], [], [30], [11, 30], [0], [0], [0], [0, 9], [0, 9, 12], [0, 12], [0], [0], [], [6, 11], [6, 11, 18], [6, 18], [6, 11], [6, 11], [6, 11], [6], [6, 11], [6, 11], [6], [6, 11], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [11, 30], [12], [25], [12], [0, 12], [], [12], [12], [12], [], [], [], [], [], [11], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []], "tok2midi": {"0": 48, "1": 49, "2": 50, "3": 51, "4": 52, "5": 53, "6": 54, "7": 55, "8": 56, "9": 57, "10": 58, "11": 59, "12": 60, "13": 61, "14": 62, "15": 63, "16": 64, "17": 65, "18": 66, "19": 67, "20": 68, "21": 69, "22": 70, "23": 71, "24": 72, "25": 73, "26": 74, "27": 75, "28": 76, "29": 77, "30": 78, "31": 79, "32": 80, "33": 81, "34": 82, "35": 83, "36": 84, "37": 85, "38": 86, "39": 87, "40": 88, "41": 89, "42": 90, "43": 91}, "n_tokens": 44, "fps": 8, "midi_lo": 48, "midi_hi": 91}
piano/samples/48.mp3 ADDED
Binary file (25.3 kB). View file
 
piano/samples/53.mp3 ADDED
Binary file (23 kB). View file
 
piano/samples/60.mp3 ADDED
Binary file (20.2 kB). View file
 
piano/samples/65.mp3 ADDED
Binary file (17.3 kB). View file
 
piano/samples/69.mp3 ADDED
Binary file (16.6 kB). View file
 
piano/samples/74.mp3 ADDED
Binary file (15.5 kB). View file
 
piano/samples/79.mp3 ADDED
Binary file (15.6 kB). View file
 
piano/samples/84.mp3 ADDED
Binary file (14.8 kB). View file
 
piano/samples/89.mp3 ADDED
Binary file (14.4 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==6.15.2
2
+ numpy>=1.24
3
+ torch>=2.2 # the bottom "Mixture of Experts" experiment (lazy-loaded)
4
+ transformers>=4.40 # SpikeWhale config/model + SpikeTokenizer (agents/modmind/)
web/README.md ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "Quazim0t0's 🍄 Thousand Token Wood Entry"
3
+ emoji: 🍄
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 6.15.2
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # ⚔️ Modular Mind: Boss Fight
14
+
15
+ A mini **Dark-Souls-style** duel where the boss is controlled by a **Modular Mind** —
16
+ a handful of tiny neural *specialists* that communicate through a **shared latent**
17
+ (a `RecursiveLink` bridge) and a *coordinator* that reads the latent to choose the
18
+ boss's next move. **The brain was trained by self-play reinforcement learning** — its
19
+ tactics emerged from playing thousands of duels, nothing is scripted.
20
+
21
+ You play the **Fire Knight**. Defeat the **Demon Slime**.
22
+
23
+ ## How the Modular Mind works
24
+
25
+ This is the [ModularMind-on-V2](https://github.com/your-username/ModularMind) concept at *specialist scale*: instead of one
26
+ monolithic policy, six small networks each handle one concern and talk to each other
27
+ through a latent channel.
28
+
29
+ ```
30
+ game state ─▶ ┌──────────────┐ each specialist emits a latent
31
+ │ Aggressor │──┐ (LatentProjection) and, if it OWNS
32
+ │ Stalker │──┤ an action, a "drive" for that action
33
+ │ Survivor │──┤
34
+ │ Baiter │──┤ ┌───────────────┐ ┌─────────────┐
35
+ │ Punisher (M) │──┼─ sum ─▶│ RecursiveLink │─────▶│ Coordinator │─▶ action
36
+ │ Enrage (M) │──┘ │ ReGLU+residual│ shared│ read-out │
37
+ └──────────────┘ └───────────────┘ latent└─────────────┘
38
+ ```
39
+
40
+ - **Four action-owning specialists** push their move's score directly:
41
+ **Aggressor → CLEAVE**, **Stalker → APPROACH**, **Survivor → RETREAT**, **Baiter → IDLE**.
42
+ - **Two modulators (M)** — **Punisher** ("the player is open!") and **Enrage**
43
+ ("we're low on HP — go berserk") — **own no action**. Their *only* way to affect the
44
+ fight is the latent they write into the shared `RecursiveLink`, which the coordinator
45
+ turns into modulation. So training has to *learn to use the latent channel* — the whole
46
+ point of the architecture.
47
+
48
+ The right-hand panel shows all of this live: each specialist's activity, the shared
49
+ latent bridge, and the coordinator's modulation, for every decision the boss makes.
50
+
51
+ ## What emerged from training
52
+
53
+ Trained on a reward that values *dealing damage* and *pressuring in range* over
54
+ playing it safe (landing a cleave ≫ whiffing, and stalling / staying out of range is
55
+ penalised), the boss learned an **aggressive pressure** style:
56
+
57
+ - **closes the distance** when you're far or at mid-range,
58
+ - **cleaves on contact** — once you're in range and it's off cooldown it commits to a
59
+ lunging swing essentially every time (verified: in-range attack-rate ≈ **0.8–1.0**),
60
+ - **retreats only when it can't swing** (mid-cooldown) to reset spacing,
61
+ - **blocks your punish** — a **Defender** specialist raises a guard (negating ~90% of
62
+ your melee) when you swing at it and it can't cleave back,
63
+ - **punishes your recovery** and gets **even more aggressive at low HP** — the Enrage
64
+ modulator raises CLEAVE through the shared latent.
65
+
66
+ It reaches a **~55–65% win rate** against a near-optimal scripted dodger (avg reward
67
+ +8, up from −12 before the reward was tuned for aggression). Against a human it's a
68
+ fair, readable fight: **dodge the red telegraph, then punish the recovery.**
69
+
70
+ > The earlier version of the brain learned a degenerate "space forever, never commit"
71
+ > policy that *technically* won but barely attacked — so the trainer now selects the
72
+ > checkpoint on **win-rate + in-range attack-rate**, and a non-attacking policy can no
73
+ > longer be saved. (`behavior()` in `train.py` measures this directly.)
74
+
75
+ ## It learns from your fights (online finetuning)
76
+
77
+ The model is tiny, so a gradient step is microseconds — the boss finetunes from real
78
+ play **on the free CPU**. Each HARD-tier fight is logged (state, action, HP per boss
79
+ decision) and POSTed to `/learn`; the server rebuilds per-decision rewards (damage
80
+ dealt − taken, + kill / − death), computes REINFORCE returns, and takes **one Adam
81
+ step** ([`mm_grad.py`](mm_grad.py), numpy backprop verified against PyTorch to ~1e-8).
82
+ A frozen copy of the sim-trained weights anchors the update so it can't drift into
83
+ nonsense; the adapted weights feed straight back into the live boss.
84
+
85
+ - **On by default, in-memory.** Set `MM_ONLINE=0` to disable.
86
+ - **Persistent across restarts:** add Space secrets `HF_TOKEN` (write) and
87
+ `MM_DATASET_REPO` (e.g. `you/boss-fight-online`) and the adapted weights are pushed
88
+ to / pulled from that Dataset. Only HARD-tier fights train (keeps the data on-policy).
89
+
90
+ ## Difficulty = the trained brain's decision-noise
91
+
92
+ The difficulty selector doesn't change the boss's stats — it runs the **same trained
93
+ Modular Mind at a different *mistake-rate*** (`explore` = probability of a random legal
94
+ action). vs a near-optimal scripted dodger:
95
+
96
+ | Tier | Mistake-rate | Boss win-rate | Feel |
97
+ |------|------|------|------|
98
+ | **Easy** | 50% | ~0.35 | erratic, leaves big openings — beatable |
99
+ | **Normal** | 22% | ~0.65 | competent pressure, occasional slip |
100
+ | **Hard** | 4% | ~0.95 | closes in, blocks your punish, relentless |
101
+
102
+ The browser sends the chosen tier with every decision; the server routes it through
103
+ `modular_mind.decide` with that tier's mistake-rate. *(Originally Easy/Normal were
104
+ genuinely-undertrained checkpoints — but once the boss learned to BLOCK it dominates
105
+ the sim almost immediately, so there's no longer a weak checkpoint; decision-noise is
106
+ the controllable, honest dial. `train.py` still snapshots checkpoints if you want them.)*
107
+
108
+ ## Controls
109
+
110
+ | Key | Action |
111
+ |-----|--------|
112
+ | ← → | Move |
113
+ | Space | Roll / dodge (i-frames, costs stamina) |
114
+ | J | Attack |
115
+ | K (hold) | **Block** — cuts incoming damage to 20% and drains stamina; if stamina hits 0 your guard **breaks** into a stagger |
116
+ | M / 🔊 | Mute / unmute music + SFX |
117
+
118
+ Background music and combat SFX play during the fight (a random track per fight,
119
+ plus per-action sound effects). Audio is served statically from `audio/`.
120
+
121
+ You begin each fight with a one-time **Aegis shield** (a cyan bubble + the *Aegis*
122
+ HUD bar): for the first few seconds it absorbs **all** damage so you can learn the
123
+ controls. Once it fades it does **not** come back.
124
+
125
+ > Click **Enter the Fog**, then click the game once so it has keyboard focus.
126
+
127
+ ## Architecture / files
128
+
129
+ | File | Role |
130
+ |------|------|
131
+ | `app.py` | Gradio Space: serves the game, exposes the trained brain at `/decide` |
132
+ | `modular_mind.py` | **numpy** inference of the trained Modular Mind (no torch at runtime) |
133
+ | `mm_torch.py` | the trainable Modular Mind (specialists + RecursiveLink + coordinator) |
134
+ | `train.py` | self-play **REINFORCE** trainer → `mm_weights.npz` |
135
+ | `mm_grad.py` | pure-numpy forward+backward (REINFORCE gradient), verified vs torch — the online learner |
136
+ | `online.py` | finetunes the HARD brain from real player fights; optional HF-Dataset persistence |
137
+ | `duel_sim.py` | the headless duel simulator (the RL environment) |
138
+ | `features.py` | shared feature/action definitions (single source of truth) |
139
+ | `web/` | the HTML5 canvas game (60fps render; calls `/decide` at decision points) |
140
+ | `audio/` | background music (`mp3/`) and sound effects (`sfx/`), served statically |
141
+ | `serve_local.py` | run the whole thing locally **without gradio** (stdlib + numpy) |
142
+
143
+ The model is **tiny** (~4.5k parameters) and inference is pure numpy, so the Space
144
+ needs only `gradio` + `numpy` and starts instantly.
145
+
146
+ ## Run / retrain locally
147
+
148
+ ```bash
149
+ pip install -r requirements.txt # gradio + numpy (runtime)
150
+ python app.py # the Space, locally
151
+ # or, with no gradio at all:
152
+ python serve_local.py # http://localhost:7861
153
+
154
+ # retrain the boss (needs torch):
155
+ pip install torch
156
+ python train.py # -> mm_weights.npz (+ train_log.json)
157
+ ```
158
+
159
+ ## Credits
160
+
161
+ - **Sprites:** *Fire Knight* and *Demon Slime* free asset packs by **LuizMelo**
162
+ (itch.io). Please check their licenses for your own use.
163
+ - **Audio:** background music tracks (`audio/mp3`) and combat SFX (`audio/sfx`).
164
+ Check the licenses of your audio packs before publishing.
165
+ - **Brain:** the *Modular Mind* concept (latent-communicating specialists via
166
+ `RecursiveLink`), trained here at specialist scale by self-play RL.
167
+
168
+ Built for a HuggingFace hackathon.
web/game.css ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Modular Mind: Boss Fight -- dark-souls-ish UI */
2
+ * { box-sizing: border-box; }
3
+ #mm-root {
4
+ --bg:#0d0b10; --panel:#15121b; --edge:#2a2436; --ink:#e8e2d6; --dim:#8a8198;
5
+ --hp:#a01b1b; --hpe:#d23b2f; --php:#c8a24a; --stam:#3a8f5a; --accent:#caa15a;
6
+ font-family: "Trebuchet MS", system-ui, sans-serif;
7
+ color: var(--ink); display:flex; gap:14px; flex-wrap:wrap;
8
+ justify-content:center; align-items:flex-start; padding:8px;
9
+ background: radial-gradient(circle at 50% 0%, #1a1622, #0d0b10 70%);
10
+ border:1px solid var(--edge); border-radius:10px; user-select:none;
11
+ }
12
+ #mm-stage { position:relative; }
13
+ #mm-canvas {
14
+ display:block; background:#000; border:1px solid var(--edge); border-radius:6px;
15
+ image-rendering: pixelated; image-rendering: crisp-edges;
16
+ width:720px; max-width:92vw; height:auto;
17
+ }
18
+ /* health bars overlaid on the stage */
19
+ .mm-bars { position:absolute; left:0; right:0; top:10px; padding:0 18px;
20
+ display:flex; flex-direction:column; gap:8px; pointer-events:none; }
21
+ .mm-bar-row { display:flex; align-items:center; gap:8px; }
22
+ .mm-bar-label { width:96px; font-size:12px; letter-spacing:1px; color:var(--dim);
23
+ text-transform:uppercase; text-shadow:0 1px 2px #000; }
24
+ .mm-bar { flex:1; height:14px; background:#06040a; border:1px solid #000;
25
+ box-shadow:inset 0 0 0 1px #2a2436; position:relative; overflow:hidden; }
26
+ .mm-bar > i { position:absolute; left:0; top:0; bottom:0; display:block;
27
+ transition: width .12s linear; }
28
+ .mm-fill-boss > i { background:linear-gradient(#d23b2f,#7e1414); }
29
+ .mm-fill-php > i { background:linear-gradient(#e3c06a,#9c7a2e); }
30
+ .mm-fill-stam > i { background:linear-gradient(#4fae74,#256b41); height:8px; top:3px; }
31
+ .mm-fill-shield > i { background:linear-gradient(#bff0ff,#3aa6d6); height:8px; top:3px;
32
+ box-shadow:0 0 6px rgba(95,217,255,.7); }
33
+ #mm-shield-row { transition:opacity .4s; }
34
+ .mm-boss-name { text-align:center; font-size:13px; letter-spacing:3px;
35
+ color:var(--hpe); text-transform:uppercase; margin-bottom:2px; text-shadow:0 1px 3px #000;}
36
+
37
+ #mm-mute { position:absolute; top:10px; right:12px; z-index:5; width:34px; height:34px;
38
+ border-radius:6px; border:1px solid var(--edge); background:rgba(13,11,16,.7);
39
+ color:var(--ink); font-size:16px; cursor:pointer; line-height:1; }
40
+ #mm-mute:hover { border-color:var(--accent); }
41
+
42
+ /* centre overlay messages (YOU DIED / VICTORY / start) */
43
+ #mm-overlay { position:absolute; inset:0; display:flex; flex-direction:column;
44
+ align-items:center; justify-content:center; text-align:center; gap:14px;
45
+ background:rgba(4,3,7,.55); backdrop-filter:blur(1px); cursor:pointer; }
46
+ #mm-overlay.hidden { display:none; }
47
+ #mm-big { font-size:52px; letter-spacing:8px; font-weight:bold;
48
+ text-shadow:0 2px 18px #000; }
49
+ #mm-big.died { color:#9a1b1b; }
50
+ #mm-big.win { color:#caa15a; }
51
+ #mm-sub { color:var(--ink); font-size:15px; max-width:520px; line-height:1.5; }
52
+ #mm-start-btn { padding:10px 22px; background:#1d1726; color:var(--accent);
53
+ border:1px solid var(--accent); border-radius:6px; font-size:16px; cursor:pointer;
54
+ letter-spacing:2px; text-transform:uppercase; }
55
+ #mm-start-btn:hover { background:#2a2136; }
56
+ .mm-diff { display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-content:center; }
57
+ .mm-diff-label { font-size:12px; color:var(--dim); letter-spacing:1px; text-transform:uppercase; }
58
+ .mm-diff-btn { display:flex; flex-direction:column; align-items:center; gap:1px;
59
+ padding:6px 14px; background:#161019; color:var(--dim); border:1px solid var(--edge);
60
+ border-radius:6px; cursor:pointer; font-size:14px; letter-spacing:1px; min-width:84px; }
61
+ .mm-diff-btn small { font-size:9px; opacity:.7; letter-spacing:.5px; }
62
+ .mm-diff-btn:hover { border-color:var(--accent); color:var(--ink); }
63
+ .mm-diff-btn.mm-diff-on { background:#2a1f12; color:var(--accent); border-color:var(--accent);
64
+ box-shadow:0 0 8px rgba(202,161,90,.35); }
65
+ .mm-keys { color:var(--dim); font-size:12.5px; line-height:1.9; }
66
+ .mm-keys b { color:var(--ink); background:#241d30; padding:1px 7px; border-radius:4px;
67
+ border:1px solid var(--edge); }
68
+
69
+ /* ---------- Modular Mind telemetry panel ---------- */
70
+ #mm-panel { width:330px; max-width:92vw; background:var(--panel);
71
+ border:1px solid var(--edge); border-radius:8px; padding:12px 13px; }
72
+ #mm-panel h3 { margin:0 0 2px; font-size:14px; letter-spacing:2px; color:var(--accent);
73
+ text-transform:uppercase; }
74
+ #mm-panel .mm-tag { font-size:11px; color:var(--dim); margin-bottom:10px; }
75
+ .mm-decision { display:flex; align-items:center; justify-content:space-between;
76
+ background:#0e0b14; border:1px solid var(--edge); border-radius:6px;
77
+ padding:7px 10px; margin-bottom:10px; }
78
+ .mm-decision .lbl { font-size:11px; color:var(--dim); letter-spacing:1px; }
79
+ .mm-decision .act { font-size:20px; font-weight:bold; letter-spacing:2px; color:#fff; }
80
+ .mm-phase { font-size:11px; padding:2px 8px; border-radius:10px; border:1px solid var(--edge);}
81
+ .mm-phase.p1 { color:#7fb2e6; } .mm-phase.p2 { color:#e6705f; border-color:#7e1414;
82
+ background:#1c0e0e; }
83
+
84
+ .mm-sec-title { font-size:11px; color:var(--dim); letter-spacing:1px;
85
+ text-transform:uppercase; margin:12px 0 6px; }
86
+ .mm-spec { margin-bottom:7px; }
87
+ .mm-spec .top { display:flex; justify-content:space-between; align-items:baseline; }
88
+ .mm-spec .nm { font-size:12.5px; }
89
+ .mm-spec .nm .dot { display:inline-block; width:8px; height:8px; border-radius:50%;
90
+ margin-right:6px; vertical-align:middle; }
91
+ .mm-spec .owns { font-size:10px; color:var(--dim); margin-left:5px; }
92
+ .mm-spec .mod-tag { font-size:9px; color:#e67e22; border:1px solid #5a3a16;
93
+ padding:0 5px; border-radius:8px; margin-left:5px; }
94
+ .mm-spec .val { font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums; }
95
+ .mm-spec .track { height:7px; background:#06040a; border:1px solid #000; margin-top:3px;
96
+ border-radius:3px; overflow:hidden; }
97
+ .mm-spec .track > i { display:block; height:100%; transition:width .15s; }
98
+ .mm-spec.win .nm { color:#fff; font-weight:bold; }
99
+ .mm-spec.win .track { box-shadow:0 0 6px rgba(202,161,90,.6); }
100
+
101
+ .mm-latent { display:flex; gap:2px; align-items:flex-end; height:34px; margin-top:4px;
102
+ background:#0e0b14; border:1px solid var(--edge); border-radius:4px; padding:3px; }
103
+ .mm-latent > i { flex:1; background:linear-gradient(#caa15a,#5a4422); border-radius:1px;
104
+ min-height:1px; transition:height .15s; }
105
+ .mm-flow { font-size:10.5px; color:var(--dim); margin-top:8px; line-height:1.5; }
106
+ .mm-flow b { color:var(--accent); }
107
+ .mm-note { font-size:10px; color:#6f6880; margin-top:10px; border-top:1px solid var(--edge);
108
+ padding-top:8px; line-height:1.5; }
web/game.js ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Modular Mind: Boss Fight -- HTML5 canvas duel.
2
+ * Rendering / physics / animation run at 60fps in the browser. The boss's NEXT
3
+ * ACTION is chosen by the trained Modular Mind (Python) via window.MM_DECIDE,
4
+ * called only at decision points (when the boss is free to commit) -- exactly how
5
+ * a souls boss commits to a move. Combat balance mirrors duel_sim.py so the boss
6
+ * behaves the way it was trained.
7
+ */
8
+ (function () {
9
+ "use strict";
10
+
11
+ // ---- world / balance (world units; speeds in units/sec) ----------------
12
+ const W = 900, Hgt = 420, GROUND = 372;
13
+ const CFG = {
14
+ BOSS_HP: 100, PLAYER_HP: 100,
15
+ BOSS_MOVE: 190, PLAYER_MOVE: 188, ROLL_SPEED: 230,
16
+ MIN_GAP: 46,
17
+ CLEAVE_REACH: 170, CLEAVE_DMG: 22, CLEAVE_LUNGE: 270,
18
+ CLEAVE_WINDUP: 0.30, CLEAVE_ACTIVE: 0.17, CLEAVE_RECOVER: 0.42, CLEAVE_COOLDOWN: 0.34,
19
+ PATK_REACH: 120, PATK_DMG: 6,
20
+ PATK_WINDUP: 0.13, PATK_ACTIVE: 0.10, PATK_RECOVER: 0.30,
21
+ ROLL_DUR: 0.53, ROLL_IFR_A: 0.10, ROLL_IFR_B: 0.37,
22
+ BLOCK_MIT: 0.2, STAGGER: 0.5,
23
+ BOSS_BLOCK_TIME: 0.4, BOSS_BLOCK_MIT: 0.1, // boss guard: duration + dmg taken multiplier
24
+ STAM_MAX: 100, STAM_ROLL: 28, STAM_ATK: 18, STAM_BLOCK: 24, STAM_REGEN: 26,
25
+ COMMIT_MOVE: 0.30, COMMIT_IDLE: 0.34,
26
+ THINK_MIN: 0.10,
27
+ SHIELD_TIME: 5.0, // one-time "learning grace": absorbs all damage at the
28
+ // start of the fight, then it's gone for good.
29
+ };
30
+
31
+ // ---- assets ------------------------------------------------------------
32
+ const A = window.GAME_ASSETS;
33
+ const imgs = {};
34
+ function loadImg(src) { return new Promise(r => { const i = new Image(); i.onload = () => r(i); i.src = src; }); }
35
+
36
+ // ---- audio (served statically; base injected by the server) ------------
37
+ const AUDIO_BASE = window.MM_AUDIO_BASE || "audio/";
38
+ const SFX = {
39
+ p_attack: "sfx/attack_knight.wav", p_hurt: "sfx/hurt_knight.wav",
40
+ p_roll: "sfx/jump_knight.wav", p_die: "sfx/die_knight.wav",
41
+ shield: "sfx/gem.wav", ui: "sfx/coin.wav",
42
+ b_cleave: "sfx/axe_boss.wav", b_hurt: "sfx/hurt_monster.wav",
43
+ b_die: "sfx/die_boss.wav", roar: "sfx/roar_monster.wav",
44
+ b_block: "sfx/bit_monster.wav",
45
+ };
46
+ const MUSIC = Array.from({ length: 12 }, (_, i) => `mp3/Pixel ${i + 1}.mp3`);
47
+ const sfxBuf = {};
48
+ let musicEl = null, muted = false, audioReady = false;
49
+ const aurl = p => AUDIO_BASE + encodeURI(p);
50
+
51
+ function initAudio() {
52
+ if (audioReady) return; audioReady = true;
53
+ for (const k in SFX) { const a = new Audio(aurl(SFX[k])); a.preload = "auto"; sfxBuf[k] = a; }
54
+ }
55
+ function playSfx(k, vol = 0.6) {
56
+ if (muted || !sfxBuf[k]) return;
57
+ const a = sfxBuf[k].cloneNode(); a.volume = vol; a.play().catch(() => {});
58
+ }
59
+ function startMusic() {
60
+ if (muted) return;
61
+ const track = MUSIC[Math.floor(Math.random() * MUSIC.length)];
62
+ if (musicEl) musicEl.pause();
63
+ musicEl = new Audio(aurl(track));
64
+ musicEl.volume = 0.3;
65
+ musicEl.addEventListener("ended", () => { if (!muted) startMusic(); });
66
+ musicEl.play().catch(() => {});
67
+ }
68
+ function stopMusic() { if (musicEl) { musicEl.pause(); musicEl = null; } }
69
+ function toggleMute() {
70
+ muted = !muted;
71
+ if (muted) { if (musicEl) musicEl.pause(); }
72
+ else if (musicEl) musicEl.play().catch(() => {});
73
+ const b = document.getElementById("mm-mute"); if (b) b.textContent = muted ? "🔇" : "🔊";
74
+ }
75
+
76
+ // ---- animation -----------------------------------------------------------
77
+ class Anim {
78
+ constructor(who) { this.man = A[who].manifest; this.img = imgs[who]; this.who = who;
79
+ // native facing of the source art: the knight faces RIGHT, the demon faces LEFT.
80
+ // We flip when the desired world-facing differs from the art's native facing.
81
+ this.native = who === "boss" ? -1 : 1;
82
+ this.name = "idle"; this.t = 0; this.idx = 0; this.done = false; this.loop = true; this.speed = 1; this.rev = false; }
83
+ set(name, loop = true, speed = 1, rev = false) {
84
+ // don't restart an animation that is already playing (it is re-requested
85
+ // every frame by the state machine); a real change of name restarts it.
86
+ if (this.name === name) { this.loop = loop; this.speed = speed; this.rev = rev; return; }
87
+ this.name = name; this.loop = loop; this.speed = speed; this.rev = rev; this.t = 0; this.done = false;
88
+ this.idx = rev ? this.man.anims[name].frames - 1 : 0;
89
+ }
90
+ update(dt) {
91
+ const a = this.man.anims[this.name]; if (!a) return;
92
+ this.t += dt * a.fps * this.speed;
93
+ if (this.t >= 1) {
94
+ const steps = Math.floor(this.t); this.t -= steps;
95
+ this.idx += steps * (this.rev ? -1 : 1);
96
+ if (this.rev) {
97
+ if (this.idx < 0) {
98
+ if (this.loop) this.idx = ((this.idx % a.frames) + a.frames) % a.frames;
99
+ else { this.idx = 0; this.done = true; }
100
+ }
101
+ } else if (this.idx >= a.frames) {
102
+ if (this.loop) this.idx %= a.frames;
103
+ else { this.idx = a.frames - 1; this.done = true; }
104
+ }
105
+ }
106
+ }
107
+ progress() { const a = this.man.anims[this.name]; return (this.idx + this.t) / a.frames; }
108
+ draw(ctx, x, facing, scale) {
109
+ const m = this.man, a = m.anims[this.name];
110
+ const sx = this.idx * m.frameW, sy = a.row * m.frameH;
111
+ const dw = m.frameW * scale, dh = m.frameH * scale;
112
+ const dy = GROUND - m.footY * scale;
113
+ // translate to the entity's feet/centre, mirror by facing, draw so the
114
+ // frame's centreX maps to local 0 and footY maps to GROUND.
115
+ ctx.save();
116
+ ctx.translate(x, dy);
117
+ // flip when the wanted world-facing differs from the art's native facing
118
+ ctx.scale(facing * this.native < 0 ? -1 : 1, 1);
119
+ ctx.drawImage(this.img, sx, sy, m.frameW, m.frameH, -m.centerX * scale, 0, dw, dh);
120
+ ctx.restore();
121
+ }
122
+ }
123
+
124
+ // ---- entities ----------------------------------------------------------
125
+ const boss = { x: 250, hp: CFG.BOSS_HP, facing: 1, anim: null,
126
+ state: "idle", t: 0, cd: 0, think: 0, pending: false, action: "IDLE",
127
+ actT: 0, cleavePhase: "", cleaveHit: false, scale: 1.7, telemetry: null, blockFlash: 0 };
128
+ const player = { x: 650, hp: CFG.PLAYER_HP, stam: CFG.STAM_MAX, facing: -1, anim: null,
129
+ state: "idle", t: 0, atkHit: false, scale: 2.4, shield: 0, shieldFlash: 0 };
130
+
131
+ const keys = {};
132
+ let game = "menu"; // menu | fight | dead | win
133
+ let lastDecision = null;
134
+ let difficulty = "hard"; // easy | normal | hard (which trained brain runs the boss)
135
+ let fightLog = []; // per-decision log sent to the online learner at fight end
136
+
137
+ // ---- helpers -----------------------------------------------------------
138
+ const dist = () => Math.abs(player.x - boss.x);
139
+ // Characters face the way they MOVE while walking/running/rolling, and face
140
+ // their opponent when idle or attacking (so swings connect & telegraphs aim
141
+ // correctly). Called once per frame after the updates, before rendering.
142
+ function updateFacings() {
143
+ const towardBossFromPlayer = boss.x >= player.x ? 1 : -1;
144
+ if (player.state === "run") {
145
+ const left = keys["ArrowLeft"] || keys["a"] || keys["A"];
146
+ const right = keys["ArrowRight"] || keys["d"] || keys["D"];
147
+ if (right && !left) player.facing = 1;
148
+ else if (left && !right) player.facing = -1; // both/neither -> keep
149
+ } else if (player.state === "roll") {
150
+ player.facing = player.rollDir; // roll in the direction it travels
151
+ } else if (player.state !== "dead") {
152
+ player.facing = towardBossFromPlayer; // idle/attack/block/hit face the boss
153
+ }
154
+
155
+ // the boss is locked onto the player: it ALWAYS faces you (it never turns its
156
+ // back). Retreat is rendered as a backstep (reversed walk) instead of a turn.
157
+ if (boss.state !== "dead") boss.facing = player.x >= boss.x ? 1 : -1;
158
+ }
159
+ function clampX(o) { o.x = Math.max(40, Math.min(W - 40, o.x)); }
160
+ function enforceGap() {
161
+ // a roll lets the player dodge THROUGH the boss to the other side; the boss's
162
+ // always-face-the-player logic then spins it around. Otherwise bodies don't overlap.
163
+ if (player.state === "roll") return;
164
+ if (dist() < CFG.MIN_GAP) {
165
+ if (player.x < boss.x) player.x = boss.x - CFG.MIN_GAP; else player.x = boss.x + CFG.MIN_GAP;
166
+ clampX(player);
167
+ }
168
+ }
169
+ const phase = () => (boss.hp < CFG.BOSS_HP * 0.5 ? 2 : 1);
170
+
171
+ // ===================== PLAYER =====================
172
+ function playerUpdate(dt) {
173
+ if (player.shield > 0) player.shield = Math.max(0, player.shield - dt);
174
+ if (player.shieldFlash > 0) player.shieldFlash = Math.max(0, player.shieldFlash - dt);
175
+ if (player.state === "dead") { player.anim.set("death", false); return; }
176
+ player.stam = Math.min(CFG.STAM_MAX, player.stam + CFG.STAM_REGEN * dt);
177
+
178
+ // committed states
179
+ if (player.state === "roll") {
180
+ player.t += dt; player.x += player.rollDir * CFG.ROLL_SPEED * dt; clampX(player); enforceGap();
181
+ if (player.t >= CFG.ROLL_DUR) { player.state = "idle"; }
182
+ return;
183
+ }
184
+ if (player.state === "attack") {
185
+ player.t += dt;
186
+ const a0 = CFG.PATK_WINDUP, a1 = CFG.PATK_WINDUP + CFG.PATK_ACTIVE;
187
+ if (player.t >= a0 && player.t < a1 && !player.atkHit) {
188
+ if (dist() <= CFG.PATK_REACH) { hitBoss(CFG.PATK_DMG); player.atkHit = true; }
189
+ }
190
+ if (player.t >= a1 + CFG.PATK_RECOVER) player.state = "idle";
191
+ return;
192
+ }
193
+ if (player.state === "stagger") { player.t += dt; if (player.t >= CFG.STAGGER) player.state = "idle"; return; }
194
+ if (player.state === "hit") { player.t += dt; if (player.anim.done) player.state = "idle"; }
195
+
196
+ // free: read input
197
+ let moving = false;
198
+ const left = keys["ArrowLeft"] || keys["a"] || keys["A"];
199
+ const right = keys["ArrowRight"] || keys["d"] || keys["D"];
200
+ const blocking = keys["k"] || keys["K"];
201
+ if (blocking) { player.state = "block"; }
202
+ else if (player.state === "block") { player.state = "idle"; }
203
+
204
+ if (player.state !== "block") {
205
+ if (left) { player.x -= CFG.PLAYER_MOVE * dt; moving = true; }
206
+ if (right) { player.x += CFG.PLAYER_MOVE * dt; moving = true; }
207
+ clampX(player); enforceGap();
208
+ player.state = moving ? "run" : "idle";
209
+ }
210
+ }
211
+ function playerAttack() {
212
+ if (["attack", "roll", "stagger", "hit", "dead"].includes(player.state)) return;
213
+ if (player.stam < CFG.STAM_ATK) return;
214
+ player.state = "attack"; player.t = 0; player.atkHit = false; player.stam -= CFG.STAM_ATK;
215
+ playSfx("p_attack", 0.5);
216
+ }
217
+ function playerRoll() {
218
+ if (["roll", "stagger", "hit", "dead"].includes(player.state)) return;
219
+ if (player.stam < CFG.STAM_ROLL) return;
220
+ player.state = "roll"; player.t = 0; player.stam -= CFG.STAM_ROLL;
221
+ playSfx("p_roll", 0.5);
222
+ player.rollDir = -player.facing; // roll backward (away from boss) by default
223
+ if (keys["ArrowLeft"] || keys["a"]) player.rollDir = -1;
224
+ if (keys["ArrowRight"] || keys["d"]) player.rollDir = 1;
225
+ }
226
+ function playerIframe() {
227
+ return player.state === "roll" && player.t >= CFG.ROLL_IFR_A && player.t <= CFG.ROLL_IFR_B;
228
+ }
229
+ function playerRecovering() {
230
+ if (player.state === "stagger") return true;
231
+ if (player.state === "attack" && player.t >= CFG.PATK_WINDUP + CFG.PATK_ACTIVE) return true;
232
+ if (player.state === "roll" && player.t > CFG.ROLL_IFR_B) return true;
233
+ return false;
234
+ }
235
+ function setPlayerAnim() {
236
+ const s = player.state;
237
+ if (s === "dead") player.anim.set("death", false);
238
+ else if (s === "hit") player.anim.set("take_hit", false);
239
+ else if (s === "stagger") player.anim.set("take_hit", false);
240
+ else if (s === "attack") player.anim.set("attack", false, 1.25);
241
+ else if (s === "roll") player.anim.set("roll", false, 1.1);
242
+ else if (s === "block") player.anim.set("defend", true);
243
+ else if (s === "run") player.anim.set("run", true);
244
+ else player.anim.set("idle", true);
245
+ }
246
+
247
+ // ===================== BOSS =====================
248
+ function hitBoss(dmg) {
249
+ if (boss.state === "dead") return;
250
+ // the boss's guard negates most of an incoming melee (no flinch either)
251
+ if (boss.state === "block") {
252
+ boss.blockFlash = 0.18; playSfx("b_block", 0.5);
253
+ boss.hp = Math.max(0, boss.hp - dmg * CFG.BOSS_BLOCK_MIT);
254
+ return;
255
+ }
256
+ const wasPhase1 = boss.hp >= CFG.BOSS_HP * 0.5;
257
+ boss.hp = Math.max(0, boss.hp - dmg);
258
+ if (boss.hp <= 0) { boss.state = "dead"; boss.anim.set("death", false); playSfx("b_die", 0.8); return; }
259
+ playSfx("b_hurt", 0.5);
260
+ if (wasPhase1 && boss.hp < CFG.BOSS_HP * 0.5) playSfx("roar", 0.8); // phase-2 enrage roar
261
+ // hyperarmor during a cleave: damage lands but no flinch
262
+ if (boss.state !== "cleave") { boss.state = "hit"; boss.t = 0; boss.anim.set("take_hit", false); }
263
+ }
264
+ function hitPlayer(dmg) {
265
+ if (player.state === "dead" || playerIframe()) return;
266
+ if (player.shield > 0) { player.shieldFlash = 0.22; playSfx("shield", 0.5); return; } // grace shield absorbs it
267
+ let d = dmg;
268
+ if (player.state === "block") {
269
+ player.stam -= CFG.STAM_BLOCK;
270
+ if (player.stam <= 0) { player.stam = 0; player.state = "stagger"; player.t = 0; }
271
+ else { d *= CFG.BLOCK_MIT; }
272
+ }
273
+ player.hp = Math.max(0, player.hp - d);
274
+ if (player.hp <= 0) { player.state = "dead"; player.t = 0; player.anim.set("death", false); playSfx("p_die", 0.8); return; }
275
+ playSfx("p_hurt", 0.5);
276
+ if (player.state !== "stagger" && player.state !== "block") { player.state = "hit"; player.t = 0; }
277
+ }
278
+
279
+ function bossState() {
280
+ return {
281
+ arenaW: W, cleaveReach: CFG.CLEAVE_REACH,
282
+ bossX: boss.x, playerX: player.x,
283
+ bossHP: boss.hp / CFG.BOSS_HP, playerHP: player.hp / CFG.PLAYER_HP,
284
+ bossCooldown: boss.cd > 0 ? boss.cd : 0,
285
+ difficulty: difficulty,
286
+ playerAttacking: player.state === "attack",
287
+ playerApproaching: (player.state === "run") && (Math.sign(player.facing) === Math.sign(boss.x - player.x)),
288
+ playerRecovering: playerRecovering(),
289
+ playerBlocking: player.state === "block",
290
+ playerThreat: player.state === "attack" && dist() <= CFG.PATK_REACH + 12,
291
+ };
292
+ }
293
+
294
+ async function requestDecision() {
295
+ boss.pending = true;
296
+ const st = bossState();
297
+ let tel;
298
+ try { tel = await window.MM_DECIDE(st); }
299
+ catch (e) { tel = fallbackBrain(st); }
300
+ boss.pending = false;
301
+ if (boss.state === "dead" || game !== "fight") return;
302
+ boss.telemetry = tel; lastDecision = tel;
303
+ applyBossAction(tel.action);
304
+ // log this decision for the online learner (state + action + HP at decision time)
305
+ fightLog.push({ state: st, action: tel.action, bossHP: st.bossHP, playerHP: st.playerHP });
306
+ updatePanel(tel);
307
+ }
308
+ function applyBossAction(action) {
309
+ boss.action = action; boss.actT = 0;
310
+ if (action === "CLEAVE" && boss.cd <= 0) { startCleave(); }
311
+ else if (action === "APPROACH") { boss.state = "approach"; }
312
+ else if (action === "RETREAT") { boss.state = "retreat"; }
313
+ else if (action === "BLOCK") { startBlock(); }
314
+ else { boss.state = "idle"; }
315
+ }
316
+ function startCleave() {
317
+ boss.state = "cleave"; boss.cleavePhase = "windup"; boss.t = 0; boss.cleaveHit = false;
318
+ const sp = phase() === 2 ? 1.25 : 1.0;
319
+ boss.anim.set("cleave", false, sp);
320
+ playSfx("b_cleave", 0.6);
321
+ }
322
+ function startBlock() {
323
+ boss.state = "block"; boss.t = 0; boss.blockFlash = 0;
324
+ boss.anim.set("idle", true); // no block frame in the sheet -> braced idle + guard FX
325
+ playSfx("b_block", 0.35);
326
+ }
327
+
328
+ function bossUpdate(dt) {
329
+ if (boss.cd > 0) boss.cd -= dt;
330
+ if (boss.state === "dead") { boss.anim.set("death", false); return; }
331
+ const ph = phase();
332
+ const moveMul = ph === 2 ? 1.18 : 1.0;
333
+
334
+ if (boss.state === "hit") {
335
+ boss.t += dt; if (boss.anim.done) { boss.state = "idle"; boss.think = CFG.THINK_MIN; }
336
+ boss.anim.set("take_hit", false); return;
337
+ }
338
+ if (boss.state === "block") {
339
+ boss.t += dt; if (boss.blockFlash > 0) boss.blockFlash -= dt;
340
+ boss.anim.set("idle", true);
341
+ if (boss.t >= CFG.BOSS_BLOCK_TIME) { boss.state = "idle"; boss.actT = 0; boss.think = CFG.THINK_MIN; }
342
+ return;
343
+ }
344
+ if (boss.state === "cleave") { cleaveUpdate(dt, ph); return; }
345
+
346
+ if (boss.state === "approach" || boss.state === "retreat") {
347
+ boss.actT += dt;
348
+ const sign = boss.state === "approach" ? 1 : -1;
349
+ const toward = player.x > boss.x ? 1 : -1;
350
+ boss.x += sign * toward * CFG.BOSS_MOVE * moveMul * dt;
351
+ clampX(boss); enforceGap();
352
+ // approach = walk forward; retreat = reversed walk so it backsteps (feet move
353
+ // backward) while still facing the player, instead of moonwalking.
354
+ const backstep = boss.state === "retreat";
355
+ boss.anim.set("walk", true, backstep ? 1.0 : 1.1, backstep);
356
+ if (boss.actT >= CFG.COMMIT_MOVE) tryDecide();
357
+ return;
358
+ }
359
+
360
+ // idle
361
+ boss.anim.set("idle", true);
362
+ boss.actT += dt;
363
+ if (boss.actT >= CFG.COMMIT_IDLE) tryDecide();
364
+ }
365
+ function tryDecide() {
366
+ if (boss.pending) return;
367
+ if (boss.think > 0) { boss.think -= 1 / 60; return; }
368
+ requestDecision();
369
+ boss.think = CFG.THINK_MIN;
370
+ }
371
+ function cleaveUpdate(dt, ph) {
372
+ boss.t += dt;
373
+ const spMul = ph === 2 ? 1.25 : 1.0;
374
+ const wu = CFG.CLEAVE_WINDUP / spMul, ac = CFG.CLEAVE_ACTIVE / spMul, rc = CFG.CLEAVE_RECOVER / spMul;
375
+ if (boss.t < wu) { boss.cleavePhase = "windup"; }
376
+ else if (boss.t < wu + ac) {
377
+ boss.cleavePhase = "active";
378
+ const toward = player.x > boss.x ? 1 : -1;
379
+ boss.x += toward * CFG.CLEAVE_LUNGE * spMul * dt; clampX(boss); enforceGap();
380
+ if (!boss.cleaveHit && dist() <= CFG.CLEAVE_REACH && !playerIframe()) {
381
+ hitPlayer(CFG.CLEAVE_DMG); boss.cleaveHit = true;
382
+ }
383
+ } else if (boss.t < wu + ac + rc) { boss.cleavePhase = "recover"; }
384
+ else {
385
+ boss.cd = CFG.CLEAVE_COOLDOWN; boss.state = "idle"; boss.actT = 0;
386
+ boss.think = CFG.THINK_MIN;
387
+ }
388
+ }
389
+
390
+ // ---- JS fallback brain (only if the Python call fails) -----------------
391
+ function fallbackBrain(s) {
392
+ const inRange = Math.abs(s.playerX - s.bossX) <= s.cleaveReach;
393
+ const ready = !(s.bossCooldown > 0);
394
+ let action = "IDLE";
395
+ if (inRange && ready) action = "CLEAVE";
396
+ else if (!inRange) action = "APPROACH";
397
+ else if (inRange && !ready) action = "RETREAT";
398
+ const mk = a => ({ name: a, owns: null, color: "#888", drive: 0, latent_norm: 0 });
399
+ return { action, phase: s.bossHP < 0.5 ? 2 : 1, trained: false, fallback: true,
400
+ specialists: [], base_drive: {}, modulation: {}, final_drive: {}, probs: {},
401
+ legal: {}, shared_latent: [] };
402
+ }
403
+
404
+ // ===================== RENDER =====================
405
+ function drawArena(ctx) {
406
+ // sky
407
+ const g = ctx.createLinearGradient(0, 0, 0, Hgt);
408
+ g.addColorStop(0, "#241726"); g.addColorStop(0.6, "#160f1c"); g.addColorStop(1, "#0a0710");
409
+ ctx.fillStyle = g; ctx.fillRect(0, 0, W, Hgt);
410
+ // distant pillars
411
+ ctx.fillStyle = "rgba(40,30,52,.6)";
412
+ for (let i = 0; i < 6; i++) { const x = 60 + i * 150; ctx.fillRect(x, 120, 46, GROUND - 120); }
413
+ // ground
414
+ ctx.fillStyle = "#0c0a12"; ctx.fillRect(0, GROUND, W, Hgt - GROUND);
415
+ ctx.fillStyle = "rgba(202,161,90,.10)"; ctx.fillRect(0, GROUND, W, 3);
416
+ // fog
417
+ ctx.fillStyle = "rgba(160,27,27,.05)"; ctx.fillRect(0, GROUND - 40, W, 40);
418
+ }
419
+ function drawShadow(ctx, x, w) {
420
+ ctx.save(); ctx.globalAlpha = .35; ctx.fillStyle = "#000";
421
+ ctx.beginPath(); ctx.ellipse(x, GROUND + 2, w, 7, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();
422
+ }
423
+ function drawTelegraph(ctx) {
424
+ if (boss.state === "cleave" && boss.cleavePhase === "windup") {
425
+ const reach = CFG.CLEAVE_REACH;
426
+ ctx.save();
427
+ ctx.fillStyle = "rgba(210,59,47,.18)";
428
+ const x0 = boss.facing > 0 ? boss.x : boss.x - reach;
429
+ ctx.fillRect(x0, GROUND - 90, reach, 90);
430
+ ctx.strokeStyle = "rgba(210,59,47,.55)"; ctx.lineWidth = 2; ctx.strokeRect(x0, GROUND - 90, reach, 90);
431
+ ctx.restore();
432
+ }
433
+ }
434
+
435
+ function drawShield(ctx) {
436
+ if (player.shield <= 0) return;
437
+ const frac = player.shield / CFG.SHIELD_TIME;
438
+ const cx = player.x, cy = GROUND - 52;
439
+ const pulse = 1 + 0.05 * Math.sin(performance.now() / 140);
440
+ const rx = 46 * pulse, ry = 64 * pulse;
441
+ ctx.save();
442
+ // soft fill
443
+ ctx.globalAlpha = 0.10 + 0.16 * frac + (player.shieldFlash > 0 ? 0.4 : 0);
444
+ ctx.fillStyle = "#5fd9ff";
445
+ ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.fill();
446
+ // ring
447
+ ctx.globalAlpha = 0.5 + 0.4 * frac + (player.shieldFlash > 0 ? 0.4 : 0);
448
+ ctx.lineWidth = 2; ctx.strokeStyle = "#bff0ff";
449
+ ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.stroke();
450
+ ctx.restore();
451
+ }
452
+
453
+ function drawBossGuard(ctx) {
454
+ if (boss.state !== "block") return;
455
+ const cx = boss.x + boss.facing * 52, cy = GROUND - 70;
456
+ const flash = boss.blockFlash > 0 ? 0.5 : 0;
457
+ ctx.save();
458
+ // a hexagonal guard ward in front of the boss, facing the player
459
+ ctx.translate(cx, cy);
460
+ ctx.strokeStyle = "#ff7a3c"; ctx.fillStyle = "rgba(255,120,60," + (0.12 + flash) + ")";
461
+ ctx.lineWidth = 2.5;
462
+ ctx.beginPath();
463
+ for (let i = 0; i < 6; i++) {
464
+ const a = Math.PI / 2 + i * Math.PI / 3;
465
+ const px = Math.cos(a) * 26 * boss.facing, py = Math.sin(a) * 56;
466
+ i ? ctx.lineTo(px, py) : ctx.moveTo(px, py);
467
+ }
468
+ ctx.closePath(); ctx.fill();
469
+ ctx.globalAlpha = 0.7 + flash; ctx.stroke();
470
+ ctx.restore();
471
+ }
472
+
473
+ function render() {
474
+ const ctx = window.__mmctx;
475
+ drawArena(ctx);
476
+ drawTelegraph(ctx);
477
+ drawShadow(ctx, boss.x, 64); drawShadow(ctx, player.x, 34);
478
+ // draw order by x depth feel: boss behind if further; simple: boss first
479
+ boss.anim.draw(ctx, boss.x, boss.facing, boss.scale);
480
+ drawBossGuard(ctx);
481
+ player.anim.draw(ctx, player.x, player.facing, player.scale);
482
+ drawShield(ctx);
483
+ }
484
+
485
+ // ===================== HUD / PANEL =====================
486
+ function setW(id, frac) { const e = document.getElementById(id); if (e) e.style.width = (Math.max(0, Math.min(1, frac)) * 100) + "%"; }
487
+ function updateHUD() {
488
+ setW("mm-hp-boss", boss.hp / CFG.BOSS_HP);
489
+ setW("mm-hp-php", player.hp / CFG.PLAYER_HP);
490
+ setW("mm-stam", player.stam / CFG.STAM_MAX);
491
+ const row = document.getElementById("mm-shield-row");
492
+ if (row) {
493
+ setW("mm-shield", player.shield / CFG.SHIELD_TIME);
494
+ row.style.opacity = player.shield > 0 ? "1" : "0.25";
495
+ }
496
+ }
497
+
498
+ const ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"];
499
+ function updatePanel(t) {
500
+ const dec = document.getElementById("mm-act"); if (dec) dec.textContent = t.action;
501
+ const ph = document.getElementById("mm-phase");
502
+ if (ph) { ph.textContent = "PHASE " + t.phase; ph.className = "mm-phase p" + t.phase; }
503
+ // specialists
504
+ const wrap = document.getElementById("mm-specs"); if (!wrap) return;
505
+ let maxDrive = 0.001;
506
+ (t.specialists || []).forEach(s => { if (s.drive != null) maxDrive = Math.max(maxDrive, Math.abs(s.drive)); });
507
+ let html = "";
508
+ (t.specialists || []).forEach(s => {
509
+ const isMod = s.owns === null;
510
+ const winning = (s.owns === t.action);
511
+ const val = isMod ? s.latent_norm : s.drive;
512
+ const norm = isMod ? Math.min(1, (s.latent_norm || 0) / 3) : Math.max(0, (s.drive || 0) / maxDrive);
513
+ html += `<div class="mm-spec ${winning ? "win" : ""}">
514
+ <div class="top"><span class="nm"><span class="dot" style="background:${s.color}"></span>${s.name}`
515
+ + (isMod ? `<span class="mod-tag">latent-only</span>` : `<span class="owns">→ ${s.owns}</span>`)
516
+ + `</span><span class="val">${val == null ? "" : (isMod ? "‖z‖ " : "") + val.toFixed(2)}</span></div>
517
+ <div class="track"><i style="width:${(norm * 100).toFixed(0)}%;background:${s.color}"></i></div></div>`;
518
+ });
519
+ wrap.innerHTML = html;
520
+ // shared latent bars
521
+ const lat = document.getElementById("mm-latent");
522
+ if (lat && t.shared_latent) {
523
+ const mx = Math.max(0.5, ...t.shared_latent.map(Math.abs));
524
+ lat.innerHTML = t.shared_latent.map(v =>
525
+ `<i style="height:${(Math.abs(v) / mx * 100).toFixed(0)}%;opacity:${v < 0 ? .45 : 1}"></i>`).join("");
526
+ }
527
+ const flow = document.getElementById("mm-flow");
528
+ if (flow) {
529
+ const mod = t.modulation || {};
530
+ const cl = mod.CLEAVE || 0, ap = mod.APPROACH || 0;
531
+ flow.innerHTML = t.fallback
532
+ ? `<b>offline fallback</b> — Python brain unreachable, using heuristic.`
533
+ : `RecursiveLink → coordinator modulation: <b>CLEAVE ${cl >= 0 ? "+" : ""}${cl.toFixed(2)}</b>, `
534
+ + `APPROACH ${ap >= 0 ? "+" : ""}${ap.toFixed(2)}. Modulators (Punisher/Enrage) act only here.`;
535
+ }
536
+ }
537
+
538
+ // ===================== LOOP =====================
539
+ let last = 0;
540
+ function loop(ts) {
541
+ const dt = Math.min(0.05, (ts - last) / 1000 || 0); last = ts;
542
+ if (game === "fight") {
543
+ playerUpdate(dt); bossUpdate(dt);
544
+ updateFacings();
545
+ setPlayerAnim();
546
+ player.anim.update(dt); boss.anim.update(dt);
547
+ updateHUD();
548
+ if (player.hp <= 0 && game === "fight") endGame(false);
549
+ else if (boss.hp <= 0 && game === "fight" && boss.anim.name === "death" && boss.anim.done) endGame(true);
550
+ else if (boss.hp <= 0 && game === "fight") { /* play death anim out */ }
551
+ } else {
552
+ player.anim.update(dt); boss.anim.update(dt);
553
+ }
554
+ render();
555
+ requestAnimationFrame(loop);
556
+ }
557
+
558
+ function endGame(win) {
559
+ if (win) { game = "win"; }
560
+ else { game = "dead"; }
561
+ stopMusic();
562
+ // hand the fight to the online learner (it only trains on HARD-tier fights)
563
+ if (window.MM_LEARN && fightLog.length > 1) {
564
+ try { window.MM_LEARN({ difficulty, steps: fightLog, result: { bossDied: win, playerDied: !win } }); }
565
+ catch (e) { /* best-effort */ }
566
+ }
567
+ showOverlay(win ? "win" : "dead");
568
+ }
569
+
570
+ // ===================== OVERLAY / FLOW =====================
571
+ function showOverlay(kind) {
572
+ const ov = document.getElementById("mm-overlay");
573
+ const big = document.getElementById("mm-big");
574
+ const sub = document.getElementById("mm-sub");
575
+ const btn = document.getElementById("mm-start-btn");
576
+ ov.classList.remove("hidden");
577
+ if (kind === "menu") {
578
+ big.className = ""; big.textContent = "BOSS FIGHT";
579
+ sub.innerHTML = `A Modular Mind controls the <b>Demon Slime</b>. Six tiny specialists vote through a shared latent; the trained brain picks each move. You start with a brief <b style="color:#bff0ff">Aegis shield</b> — a few seconds to learn the controls before it fades. Defeat the boss.`;
580
+ btn.textContent = "Enter the Fog";
581
+ } else if (kind === "dead") {
582
+ big.className = "died"; big.textContent = "YOU DIED";
583
+ sub.innerHTML = `The Modular Mind read you. Watch the panel — punish its <b>recovery</b>, dodge the <b>red telegraph</b>.`;
584
+ btn.textContent = "Try Again";
585
+ } else {
586
+ big.className = "win"; big.textContent = "VICTORY";
587
+ sub.innerHTML = `Demon Slime felled. You out-played a brain trained by self-play reinforcement learning.`;
588
+ btn.textContent = "Fight Again";
589
+ }
590
+ }
591
+ function startFight() {
592
+ boss.x = 250; boss.hp = CFG.BOSS_HP; boss.state = "idle"; boss.cd = 0; boss.t = 0;
593
+ boss.actT = 0; boss.pending = false; boss.action = "IDLE"; boss.think = 0;
594
+ player.x = 650; player.hp = CFG.PLAYER_HP; player.stam = CFG.STAM_MAX; player.state = "idle"; player.t = 0;
595
+ player.shield = CFG.SHIELD_TIME; player.shieldFlash = 0;
596
+ fightLog = [];
597
+ boss.anim.set("idle", true); player.anim.set("idle", true);
598
+ document.getElementById("mm-overlay").classList.add("hidden");
599
+ game = "fight";
600
+ initAudio(); playSfx("ui", 0.4); playSfx("roar", 0.7); startMusic();
601
+ }
602
+
603
+ // ===================== INPUT =====================
604
+ function bindInput() {
605
+ document.addEventListener("keydown", e => {
606
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " "].includes(e.key)) e.preventDefault();
607
+ keys[e.key] = true;
608
+ if (e.key === "m" || e.key === "M") toggleMute();
609
+ if (game !== "fight") return;
610
+ if (e.key === " ") playerRoll();
611
+ if (e.key === "j" || e.key === "J") playerAttack();
612
+ }, { passive: false });
613
+ document.addEventListener("keyup", e => { keys[e.key] = false; });
614
+ document.getElementById("mm-start-btn").addEventListener("click", startFight);
615
+ document.getElementById("mm-overlay").addEventListener("click", e => {
616
+ if (e.target.closest(".mm-diff")) return; // clicking the selector isn't "start"
617
+ if (e.target.id === "mm-start-btn") return;
618
+ if (game !== "fight") startFight();
619
+ });
620
+ const mute = document.getElementById("mm-mute");
621
+ if (mute) mute.addEventListener("click", e => { e.stopPropagation(); toggleMute(); });
622
+ // difficulty selector
623
+ document.querySelectorAll(".mm-diff-btn").forEach(btn => {
624
+ btn.addEventListener("click", e => {
625
+ e.stopPropagation();
626
+ difficulty = btn.dataset.diff;
627
+ document.querySelectorAll(".mm-diff-btn").forEach(b => b.classList.toggle("mm-diff-on", b === btn));
628
+ const tag = document.getElementById("mm-difftag");
629
+ if (tag) tag.textContent = difficulty.toUpperCase();
630
+ });
631
+ });
632
+ }
633
+
634
+ // ===================== BOOT =====================
635
+ async function boot() {
636
+ imgs.boss = await loadImg(A.boss.image);
637
+ imgs.knight = await loadImg(A.knight.image);
638
+ boss.anim = new Anim("boss"); player.anim = new Anim("knight");
639
+ const cv = document.getElementById("mm-canvas");
640
+ cv.width = W; cv.height = Hgt; window.__mmctx = cv.getContext("2d");
641
+ window.__mmctx.imageSmoothingEnabled = false;
642
+ bindInput();
643
+ showOverlay("menu");
644
+ // lightweight debug hook (handy for testing; harmless in normal play)
645
+ window.__mmDebug = {
646
+ get boss() { return boss; }, get player() { return player; },
647
+ get game() { return game; }, get keys() { return keys; },
648
+ hurtBoss: d => hitBoss(d), hurtPlayer: d => hitPlayer(d), faces: () => updateFacings(),
649
+ };
650
+ requestAnimationFrame(loop);
651
+ }
652
+ window.__mmBoot = boot;
653
+ })();
web/index.html ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="mm-root">
2
+ <div id="mm-stage">
3
+ <canvas id="mm-canvas" width="900" height="420"></canvas>
4
+ <button id="mm-mute" title="Mute (M)">🔊</button>
5
+ <div class="mm-bars">
6
+ <div class="mm-boss-name">⚔ Demon Slime ⚔</div>
7
+ <div class="mm-bar-row"><span class="mm-bar-label">Boss</span>
8
+ <div class="mm-bar mm-fill-boss"><i id="mm-hp-boss" style="width:100%"></i></div></div>
9
+ <div class="mm-bar-row"><span class="mm-bar-label">Ashen One</span>
10
+ <div class="mm-bar mm-fill-php"><i id="mm-hp-php" style="width:100%"></i></div></div>
11
+ <div class="mm-bar-row"><span class="mm-bar-label">Stamina</span>
12
+ <div class="mm-bar mm-fill-stam"><i id="mm-stam" style="width:100%"></i></div></div>
13
+ <div class="mm-bar-row" id="mm-shield-row"><span class="mm-bar-label">Aegis</span>
14
+ <div class="mm-bar mm-fill-shield"><i id="mm-shield" style="width:100%"></i></div></div>
15
+ </div>
16
+ <div id="mm-overlay">
17
+ <div id="mm-big">BOSS FIGHT</div>
18
+ <div id="mm-sub"></div>
19
+ <div id="mm-diff" class="mm-diff">
20
+ <span class="mm-diff-label">Boss brain:</span>
21
+ <button class="mm-diff-btn" data-diff="easy">Easy<small>partly trained</small></button>
22
+ <button class="mm-diff-btn" data-diff="normal">Normal<small>mid training</small></button>
23
+ <button class="mm-diff-btn mm-diff-on" data-diff="hard">Hard<small>fully trained</small></button>
24
+ </div>
25
+ <button id="mm-start-btn">Enter the Fog</button>
26
+ <div class="mm-keys">
27
+ <b>← →</b> move &nbsp; <b>Space</b> roll/dodge &nbsp; <b>J</b> attack &nbsp; <b>K</b> hold to block
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div id="mm-panel">
33
+ <h3>Modular Mind</h3>
34
+ <div class="mm-tag">6 specialists · RecursiveLink shared latent · brain: <b id="mm-difftag">HARD</b></div>
35
+ <div class="mm-decision">
36
+ <div><div class="lbl">CURRENT DECISION</div><div class="act" id="mm-act">—</div></div>
37
+ <span class="mm-phase p1" id="mm-phase">PHASE 1</span>
38
+ </div>
39
+ <div class="mm-sec-title">Specialist activity</div>
40
+ <div id="mm-specs"></div>
41
+ <div class="mm-sec-title">Shared latent · RecursiveLink bridge</div>
42
+ <div class="mm-latent" id="mm-latent"></div>
43
+ <div class="mm-flow" id="mm-flow">Awaiting first decision…</div>
44
+ <div class="mm-note">Each move is chosen by a tiny neural Modular Mind. Four specialists own an
45
+ action; two <b>modulators</b> (Punisher, Enrage) own none — they steer the fight only by what
46
+ they write into the shared latent.</div>
47
+ </div>
48
+ </div>