뉴런의 수렴 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""뉴런의 수렴 검증 (뉴런 수렴 모델링 연습의 답 포함).

문서: 09.뉴런의 수렴, 4.연습문제/09.뉴런 수렴 모델링 연습
문서의 세 수렴 회로. 수용기 1~7, 자극받은 수용기는 1, 아니면 0.
  회로 1 (수렴 없음): B = x4
  회로 2 (흥분성 수렴): A = x1 + x2, C = x6 + x7, B = x3 + x4 + x5 + A + C
  회로 3 (억제성 수렴): B = max(0, x3 + x4 + x5 - A - C)
주장:
  1. 자극 4, 3~5, 2~6, 1~7에 대한 B의 발화율이 그래프와 같다: 회로 1은 1,1,1,1, 회로 2는 1,3,5,7,
     회로 3은 1,3,1, 그리고 1~7에서 0 (그래프는 0에 가까운 값).
  2. 세 회로는 한 줄짜리 가중치 벡터 w와 활성 함수 max(0, .)로 쓸 수 있다.
  3. 회로 3에서 B가 가장 세게(3) 반응하는 자극은 정확히 {3, 4, 5} 하나다 (2^7가지 전수 조사).
  4. 회로 2에서 자극 {1}, {4}, {7}은 모두 B = 1이다. 어디가 자극됐는지 B만으로는 알 수 없다.
  5. 연습 p4: 억제 가중치를 0.5로 줄이면 1, 3, 2, 1이다.
  6. 연습 p5: 수용기 하나의 출력이 0.3이고 발화 문턱이 1이면, 수렴 없는 뉴런은 발화하지 못하고
     일곱 개를 모은 뉴런은 2.1로 문턱을 넘는다.
  7. 신호 관점: 수용기마다 서로 독립인 잡음(표준편차 sigma)이 있으면, N개를 평균한 값의 잡음은
     sigma / sqrt(N)으로 줄어든다. N = 7이면 약 2.65배 줄어든다 (몬테카를로 20,000회).
"""
import math
import random
from fractions import Fraction as F
from itertools import product

STIMULI = {"4": {4}, "3-5": {3, 4, 5}, "2-6": {2, 3, 4, 5, 6}, "1-7": {1, 2, 3, 4, 5, 6, 7}}


def as_vector(cells: set[int], level: F = F(1)) -> list[F]:
    return [level if i in cells else F(0) for i in range(1, 8)]


def circuit1(x: list[F]) -> F:
    return x[3]


def circuit2(x: list[F]) -> F:
    a = x[0] + x[1]
    c = x[5] + x[6]
    return x[2] + x[3] + x[4] + a + c


def circuit3(x: list[F], inhibit: F = F(1)) -> F:
    a = x[0] + x[1]
    c = x[5] + x[6]
    return max(F(0), x[2] + x[3] + x[4] - inhibit * (a + c))


def weighted(w: list[F], x: list[F]) -> F:
    return max(F(0), sum(wi * xi for wi, xi in zip(w, x)))


def main() -> None:
    expected = {
        "회로 1": (circuit1, [1, 1, 1, 1]),
        "회로 2": (circuit2, [1, 3, 5, 7]),
        "회로 3": (circuit3, [1, 3, 1, 0]),
    }
    for name, (fn, exp) in expected.items():
        got = [fn(as_vector(s)) for s in STIMULI.values()]
        assert got == exp, (name, got)
        print(f"[OK] {name}: 자극 4, 3~5, 2~6, 1~7 -> {[int(g) for g in got]}")

    # 주장 2: 가중치 벡터 형태
    w1 = [F(v) for v in (0, 0, 0, 1, 0, 0, 0)]
    w2 = [F(1)] * 7
    w3 = [F(v) for v in (-1, -1, 1, 1, 1, -1, -1)]
    for bits in product((0, 1), repeat=7):
        x = [F(b) for b in bits]
        assert weighted(w1, x) == circuit1(x)
        assert weighted(w2, x) == circuit2(x)
        assert weighted(w3, x) == circuit3(x)
    print("[OK] 128가지 자극 모두: 회로 = max(0, w.x)  (w1=e4, w2=모두 1, w3=(-1,-1,1,1,1,-1,-1))")

    # 주장 3: 회로 3의 최대 반응
    best = max(circuit3([F(b) for b in bits]) for bits in product((0, 1), repeat=7))
    argmax = [bits for bits in product((0, 1), repeat=7) if circuit3([F(b) for b in bits]) == best]
    assert best == 3 and argmax == [(0, 0, 1, 1, 1, 0, 0)]
    print("[OK] 회로 3의 최대 반응 3은 자극 {3,4,5}에서만 나온다")

    # 주장 4: 위치 정보 손실
    assert circuit2(as_vector({1})) == circuit2(as_vector({4})) == circuit2(as_vector({7})) == 1
    assert circuit1(as_vector({1})) == 0 and circuit1(as_vector({4})) == 1
    print("[OK] 회로 2: {1},{4},{7} 모두 B=1 (위치 구별 불가). 회로 1은 {4}에만 반응")

    # 주장 5: 연습 p4
    got = [circuit3(as_vector(s), inhibit=F(1, 2)) for s in STIMULI.values()]
    assert got == [1, 3, 2, 1], got
    print(f"[OK] 연습 p4: 억제 가중치 0.5 -> {[int(g) for g in got]}")

    # 주장 6: 연습 p5
    weak = F(3, 10)
    threshold = F(1)
    single = circuit1(as_vector(set(range(1, 8)), weak))
    pooled = circuit2(as_vector(set(range(1, 8)), weak))
    assert single < threshold <= pooled and pooled == F(21, 10)
    print(f"[OK] 연습 p5: 약한 빛 0.3 -> 수렴 없음 {float(single)} (문턱 미만), 수렴 {float(pooled)} (문턱 이상)")

    # 주장 7: 평균하면 잡음이 sqrt(N)배 줄어든다
    rng = random.Random(1)
    n, trials = 7, 20_000
    single = [rng.gauss(0, 1) for _ in range(trials)]
    pooled = [sum(rng.gauss(0, 1) for _ in range(n)) / n for _ in range(trials)]

    def rms(v):
        return math.sqrt(sum(x * x for x in v) / len(v))

    ratio = rms(single) / rms(pooled)
    assert abs(ratio - math.sqrt(n)) < 0.1, ratio
    print(f"[OK] 신호 관점: 7개 평균의 잡음 감소 {ratio:.2f}배 (sqrt(7) = {math.sqrt(n):.2f})")

    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()