#
# This file is part of ctak.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ctak. If not, see .
from pathlib import Path
import numpy as np
import torch
from torch import Tensor
from torch.nn import Embedding, Linear, Module
# (input dim, hidden dim) per board size; must match include/weights_.h
NET_DIMS: dict[int, tuple[int, int]] = {
5: (27, 64), # FIVE_INP_NUM, FIVE_DENSE_NUM
6: (38, 64), # SIX_INP_NUM, SIX_DENSE_NUM
}
def _fmt_flat(a: np.ndarray, per_line: int = 8) -> str:
a = np.ravel(np.asarray(a, dtype=np.float32))
lines: list[str] = []
for i in range(0, len(a), per_line):
chunk = ", ".join(f"{v:.9g}" for v in a[i : i + per_line])
if i + per_line < len(a):
chunk += ","
lines.append(" " + chunk)
return "\n".join(lines)
def _fmt_matrix(mat: np.ndarray, per_line: int = 8) -> str:
mat = np.asarray(mat, dtype=np.float32)
rows: list[str] = []
for r in mat:
chunks = [
", ".join(f"{v:.9g}" for v in r[i : i + per_line])
for i in range(0, len(r), per_line)
]
rows.append(" {" + ",\n ".join(chunks) + "},")
return "\n".join(rows)
def fast_tanh(x: Tensor) -> Tensor:
# Must match fast_tanh in include/nn.c: degree-11 fit of tanh on [-3, 3],
# saturating to +-1 outside.
x2 = x * x
poly = x * (
0.997136032
+ x2
* (
-0.305215124
+ x2
* (
0.0832466371
+ x2 * (-0.0139947924 + x2 * (0.00123106314 + x2 * -4.29748644e-05))
)
)
)
return torch.where(x > 3.0, 1.0, torch.where(x < -3.0, -1.0, poly))
def fast_silu(x: Tensor) -> Tensor:
# Must match fast_silu in include/nn.c: [4,2] Pade approximant with cutoff.
# The cutoff below is evaluated at x = 0 (instead of x) because torch
# evaluates both branches of where: 1 + 0.1 * x is 0 for x = -10.
xs = torch.where(x < -4.6, 0.0, x)
poly = xs * (0.5 + xs * (0.25 + xs * (0.05 + 0.004167 * xs))) / (1 + 0.1 * xs)
return torch.where(x < -4.6, 0.0, torch.where(x > 4.6, x, poly))
class Net(Module):
def __init__(self, inp_dim: int, hidden: int) -> None:
super().__init__()
# learned cell-state embedding, see nn.c
self.embedding = Embedding(7, 1)
self.hidden = Linear(inp_dim, hidden, bias=True)
self.output = Linear(hidden, 1, bias=True)
def forward(self, head: Tensor, cells: Tensor) -> Tensor:
# head: [n, 2] fixed head inputs; cells: [n, n_cells] raw cell states
# (0..6) - the only place they become anything other than integers
inp = torch.cat([head, self.embedding(cells.long()).flatten(1)], dim=1)
hidden = fast_silu(self.hidden(inp))
return fast_tanh(self.output(hidden)).squeeze(-1)
def to_c(
self,
*,
size: int,
ident: int | None,
path: Path,
note: str,
) -> None:
upper = "FIVE" if size == 5 else "SIX"
prefix = (f"cand_{ident}_" if ident is not None else "") + upper
guard = f"WEIGHTS_{upper}" + (f"_{ident}" if ident is not None else "")
E = self.embedding.weight.detach().cpu().numpy()
W = self.hidden.weight.detach().cpu().numpy()
B = self.hidden.bias.detach().cpu().numpy()
LW = self.output.weight.detach().cpu().numpy().ravel()
LB = float(self.output.bias.detach().cpu().item())
with open(path, "w") as f:
lines = [
"/* generated by nn_train.py, do not edit by hand */\n",
f"/* {note} */\n",
f"#ifndef {guard}\n#define {guard}\n",
f'#include "weights_{size}.h"\n\n',
f"const float {prefix}_embedding[{len(E)}] =\n{{\n{_fmt_flat(E, per_line=7)}\n}};\n\n",
f"const float {prefix}_dense_weights[{upper}_INP_NUM][{upper}_DENSE_NUM] NN_ALIGNED =\n{{\n{_fmt_matrix(W.T)}\n}};\n\n",
f"const float {prefix}_dense_biases[{upper}_DENSE_NUM] =\n{{\n{_fmt_flat(B)}\n}};\n\n",
f"const float {prefix}_logit_weights[{upper}_DENSE_NUM] NN_ALIGNED =\n{{\n{_fmt_flat(LW)}\n}};\n\n",
f"const float {prefix}_logit_bias = {LB:.9g};\n",
"#endif",
]
for line in lines:
_ = f.write(line)