---
jupytext:
  formats: md:myst
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
kernelspec:
  display_name: Python 3
  language: python
  name: python3
---

# 3회 실험 · 유리수의 구멍

```{admonition} 이 실험
:class: seealso

| | |
|---|---|
| **짝 서술** | {doc}`L03` |
| **실험 목록** | 실험 1 (1층) — 확대해도 채워지지 않는다 / 실험 2 (3층) — 유리수와 무리수를 구별한다 |
```

이번 회차의 대상은 공리이다. 공리는 그림으로 확인할 수 있는 것이 아니므로 실험은 두 개뿐이며, 둘 다 서술 쪽 {prf:ref}`thm-3-sqrt2`가 말하는 내용을 다른 각도에서 비추는 데 쓴다.

```{code-cell} ipython3
:tags: [hide-input]

# 공통 준비 — 색 규약과 한글 글꼴을 맞춘다
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
from fractions import Fraction

from calc_viz import 색
from calc_style import 한글글꼴설정

글꼴 = 한글글꼴설정()   # 돌려주는 값은 실제로 선택된 글꼴 이름이다
```

## 실험 1 (1층) — 확대해도 채워지지 않는다

유리수는 조밀하다. 어떤 두 실수 사이에도 유리수가 있고, 확대해도 사정은 같다.

그런데 조밀하다는 것과 빈틈이 없다는 것은 다른 말이다. 아래 세 그림은 같은 지점을 세 배율로 확대한 것이다. 배율을 올리면 분모를 키워 유리수를 더 촘촘히 찍는다. 빨간 세로선은 제곱해서 $2$가 되는 수가 있어야 할 위치이다.

```{code-cell} ipython3
# [3회] 유리수의 조밀성과 구멍
# 목적 : 유리수는 어디에나 있지만 제곱해서 2가 되는 유리수는 없다는 것을 확인한다
# 층  : 1층
# 주의 : 그림은 유한개의 유리수만 찍는다. 그림이 증명이 될 수는 없다

중심 = 2 ** 0.5

그림, 축 = plt.subplots(3, 1, figsize=(9, 6))
for i, 반경 in enumerate([0.5, 0.05, 0.005]):
    분모 = int(20 / 반경)
    값 = np.array([j / 분모
                  for j in range(int((중심 - 반경) * 분모), int((중심 + 반경) * 분모) + 1)])
    축[i].scatter(값, np.zeros_like(값), s=10, color=색["하합"], label="유리수")
    축[i].axvline(중심, color=색["차이"], label="제곱해서 2가 되는 수의 위치")
    축[i].set_xlim(중심 - 반경, 중심 + 반경)
    축[i].set_yticks([])
    축[i].set_title(f"확대 배율 {0.5 / 반경:.0f}배 (분모 {분모}까지)")
축[0].legend(loc="upper right")

plt.tight_layout()
plt.show()
```

세 그림 모두에서 빨간 선 양옆에 파란 점이 빽빽하다. 그러나 어느 그림에서도 파란 점이 빨간 선 위에 정확히 놓이지 않는다. 분모를 아무리 키워도 그렇다.

이것을 유리수로 직접 확인해 보자. 분모를 키워 가며 제곱이 $2$에 가장 가까운 분수를 찾는다.

```{code-cell} ipython3
# [3회] 제곱이 2 에 가장 가까운 분수
# 목적 : 분모를 키우면 제곱값이 2 에 가까워지지만 도달하지는 않음을 확인한다
# 층  : 1층 (Fraction 으로 정확히 계산한다. 부동소수를 쓰지 않는다)
# 주의 : 가까워지는 것과 도달하는 것은 다르다

from calc_viz import 칸   # 한글이 섞인 표의 열을 맞춘다

머리 = ["분모", "가장 가까운 분수", "제곱 − 2", "제곱 = 2 인가"]
폭 = [8, 20, 28, 14]
print(" ".join(칸(글, w) for 글, w in zip(머리, 폭)))

for 분모 in (10, 100, 1000, 10000):
    # 제곱이 2 를 넘지 않는 가장 큰 분자를 찾는다
    분자 = max(j for j in range(1, 2 * 분모) if Fraction(j, 분모) ** 2 <= 2)
    후보 = Fraction(분자, 분모)
    차이 = 후보 ** 2 - 2
    줄 = [분모, 후보, 차이, 후보 ** 2 == 2]
    print(" ".join(칸(항목, w) for 항목, w in zip(줄, 폭)))
```

`차이` 열이 정확한 분수로 나온다는 점에 유의한다. 부동소수로 계산하면 반올림 때문에 `0.0`이 나올 수도 있지만, `Fraction`은 정확히 계산하므로 차이가 결코 $0$이 되지 않는다는 것이 보인다.

```{admonition} 확인할 것
:class: tip
1. 세 그림에서 파란 점의 개수는 거의 같다. 그런데 배율은 100배까지 올라간다. 이것이 무엇을 뜻하는지 생각해 보자.
2. 두 번째 코드에서 분모를 키울 때 `제곱 − 2`가 어떻게 변하는지 확인하자. $0$이 되는 경우가 있는가.
3. 위 두 실험을 아무리 반복해도 {prf:ref}`thm-3-sqrt2`를 증명할 수 없는 이유를 말해 보자.
```

## 실험 2 (3층) — 유리수와 무리수를 구별한다

부동소수는 유리수와 무리수를 구별하지 못한다. 어떤 값이든 유한한 자릿수로 잘라 저장하므로, 저장된 값은 언제나 유리수이다.

기호 계산은 값을 자르지 않고 식의 형태로 들고 있으므로 구별이 가능하다.

```{code-cell} ipython3
# [3회] 유리수인지 아닌지를 sympy 로 판정한다
# 목적 : 부동소수로는 구별할 수 없는 것을 기호 계산으로 구별한다
# 층  : 3층
# 주의 : is_rational 은 판정 알고리즘의 결과이다. 증명이 아니다

후보 = {
    "sqrt(2)":         sp.sqrt(2),
    "sqrt(4)":         sp.sqrt(4),
    "22/7":            sp.Rational(22, 7),
    "sqrt(2)*sqrt(2)": sp.sqrt(2) * sp.sqrt(2),
    "sqrt(2)+sqrt(2)": sp.sqrt(2) + sp.sqrt(2),
}
for 이름, 값 in 후보.items():
    print(f"  {이름:18s} 정리하면 {str(sp.nsimplify(값)):12s} 유리수인가 : {값.is_rational}")
```

`sqrt(2)*sqrt(2)`가 유리수로 판정되는 것에 유의한다. 무리수끼리의 연산 결과가 언제나 무리수인 것은 아니다.

같은 값을 부동소수와 기호로 각각 다루면 결과가 갈린다.

```{code-cell} ipython3
# [3회] 부동소수와 기호 계산의 차이
# 목적 : 같은 등식이 한쪽에서는 거짓, 다른 쪽에서는 참으로 나오는 것을 확인한다
# 층  : 1층과 3층의 대조
# 주의 : 부동소수의 결과가 틀린 것이 아니라, 저장 방식이 다른 것이다

부동 = float(sp.sqrt(2))
print("부동소수로 계산하면")
print(f"  float(sqrt(2))**2 == 2 : {부동 ** 2 == 2}")
print(f"  차이                   : {부동 ** 2 - 2:.3e}")

print("\n기호로 계산하면")
print(f"  sp.sqrt(2)**2 == 2     : {sp.simplify(sp.sqrt(2) ** 2 - 2) == 0}")
```

이제 방정식 $x^2 = 2$를 두 정의역에서 각각 풀어 본다.

```{code-cell} ipython3
# [3회] 정의역을 바꾸어 방정식을 푼다
# 목적 : 같은 방정식이 실수에서는 해를 갖고 유리수에서는 갖지 않음을 확인한다
# 층  : 3층
# 주의 : 이것이 3회 4절의 결론과 같은 내용이다

x = sp.Symbol("x", real=True)
방정식 = sp.Eq(x**2, 2)

print(f"  실수 범위의 해   : {sp.solveset(방정식, x, domain=sp.S.Reals)}")
print(f"  유리수 범위의 해 : {sp.solveset(방정식, x, domain=sp.S.Rationals)}")
```

유리수 범위에서 해집합이 `EmptySet`이다. 이것이 서술 쪽 {prf:ref}`thm-3-sqrt2`가 말하는 내용이며, 동시에 3회 4절이 지적한 $\R$과 $\Q$의 차이이다.

```{admonition} 확인할 것
:class: tip
1. `float(sqrt(2))**2`가 정확히 2인지 확인하자. 아니라면 차이는 얼마인가.
2. 유리수 범위에서 해집합이 무엇인지 확인하자. 공집합인가.
3. 실험 1의 그림과 실험 2의 마지막 결과 중 어느 쪽이 3회의 정리를 뒷받침하는가.
```

```{admonition} 어느 것도 증명이 아니다
:class: warning

세 번째 확인 항목이 이 회차의 요점이다.

실험 1의 그림은 **정황**이다. 유한개의 점을 찍어 보았을 뿐이므로, 찍지 않은 어딘가에 해가 있을 가능성을 배제하지 못한다.

실험 2의 `EmptySet`은 **알고리즘의 결과**이다. `sympy`가 내부에서 어떤 판정 절차를 돌린 결과이며, 그 절차가 옳다는 것은 별도의 문제이다.

둘 다 3회 4절의 귀류법 증명을 대신하지 못한다. 증명이 하는 일은 "찾지 못했다"가 아니라 "있을 수 없다"를 보이는 것이며, 이것은 유한 번의 확인으로는 도달할 수 없다.
```

```{admonition} 짝이 되는 서술
:class: note
{doc}`L03`
```
