사인 법칙과 코사인 법칙 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""사인 법칙과 코사인 법칙 검증.

문서: 16.사인 법칙과 코사인 법칙 (예시, 정리, 증명, 예제, 카드 C1~C3)
주장 1: 무작위 삼각형에서 c² = a² + b² - 2ab cos C, a/sin A = b/sin B = c/sin C, 넓이 = ½ab sin C.
주장 2: 두 변 5, 8과 끼인각 60°이면 셋째 변은 7이다.
주장 3: 기선 10 m 양 끝에서 목표까지의 각이 60°, 70°이면 첫째 끝에서 목표까지 10 sin 70° / sin 50° ≈ 12.27 m.
주장 4: a = 6, b = 8, A = 30°이면 B ≈ 41.81° 또는 138.19°로 삼각형이 둘이다.
주장 5: C = 90°이면 코사인 법칙이 피타고라스 정리가 된다.
"""
import math
import random


def angle(opposite, s1, s2):
    return math.acos((s1 * s1 + s2 * s2 - opposite * opposite) / (2 * s1 * s2))


def main():
    rng = random.Random(16)
    for _ in range(5000):
        P = [(rng.uniform(-10, 10), rng.uniform(-10, 10)) for _ in range(3)]
        a, b, c = math.dist(P[1], P[2]), math.dist(P[0], P[2]), math.dist(P[0], P[1])
        area = abs((P[1][0] - P[0][0]) * (P[2][1] - P[0][1]) - (P[2][0] - P[0][0]) * (P[1][1] - P[0][1])) / 2
        if area < 1e-3:
            continue
        # 각은 좌표에서 벡터로 직접 잰다 (코사인 법칙을 쓰지 않고)
        def ang(Q, R1, R2):
            v1 = (R1[0] - Q[0], R1[1] - Q[1]); v2 = (R2[0] - Q[0], R2[1] - Q[1])
            return abs(math.atan2(v1[0] * v2[1] - v1[1] * v2[0], v1[0] * v2[0] + v1[1] * v2[1]))
        A, B, C = ang(P[0], P[1], P[2]), ang(P[1], P[0], P[2]), ang(P[2], P[0], P[1])
        assert math.isclose(A + B + C, math.pi)
        assert math.isclose(c * c, a * a + b * b - 2 * a * b * math.cos(C), rel_tol=1e-9, abs_tol=1e-9)
        assert math.isclose(a / math.sin(A), b / math.sin(B), rel_tol=1e-7) and math.isclose(b / math.sin(B), c / math.sin(C), rel_tol=1e-7)
        assert math.isclose(area, 0.5 * a * b * math.sin(C), rel_tol=1e-9)
    print("[OK] 주장 1: 무작위 삼각형 5,000개 (각은 좌표에서 직접 측정)")

    c = math.sqrt(25 + 64 - 2 * 5 * 8 * math.cos(math.radians(60)))
    assert math.isclose(c, 7)
    area = 0.5 * 5 * 8 * math.sin(math.radians(60))
    assert math.isclose(area, 10 * math.sqrt(3)) and f"{area:.2f}" == "17.32"
    print("[OK] 주장 2·카드 C1: 5, 8, 60° -> 7, 넓이 10√3 ≈ 17.32")

    d = 10 * math.sin(math.radians(70)) / math.sin(math.radians(50))
    assert f"{d:.2f}" == "12.27"
    print(f"[OK] 주장 3: 삼각측량 {d:.3f} m")

    sB = 8 * math.sin(math.radians(30)) / 6
    B1 = math.degrees(math.asin(sB)); B2 = 180 - B1
    assert f"{B1:.2f}" == "41.81" and f"{B2:.2f}" == "138.19" and 30 + B2 < 180
    assert f"{180 - 30 - B1:.2f}" == "108.19" and f"{180 - 30 - B2:.2f}" == "11.81"
    for B in (B1, B2):
        Cc = 180 - 30 - B
        cc = 6 * math.sin(math.radians(Cc)) / math.sin(math.radians(30))
        assert math.isclose(math.degrees(angle(6, 8, cc)), 30)
    print(f"[OK] 주장 4·카드 C3: B = {B1:.2f}° 또는 {B2:.2f}°, 두 삼각형 모두 A = 30°")

    for _ in range(100):
        a, b = rng.uniform(1, 10), rng.uniform(1, 10)
        assert math.isclose(a * a + b * b - 2 * a * b * math.cos(math.pi / 2), a * a + b * b)
    print("[OK] 주장 5: C = 90°이면 보정항 0")
    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()