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

# 7회 실험 · 적분의 정의

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

| | |
|---|---|
| **짝 서술** | {doc}`L07` |
| **실험 목록** | 1층 정의 계산 · 2층 scipy 대조 · 3층 정확한 식 · 분할 무관성 · 무작위 분할 · 그림 |
```

PART I에서 가장 중요한 실험이다. 세 층을 모두 쓴다.

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

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

from calc_viz import 상합하합, 함수그리기, 계단함수그리기, 무대, 색, 칸
from calc_style import 한글글꼴설정

글꼴 = 한글글꼴설정()

f = lambda x: x**2      # 이 회차의 대상
a, b = 0.0, 1.0
```

## 실험 1 (1층) — 정의를 그대로 옮긴 계산

7회 (7.13)을 코드로 옮긴다. $[0,1]$을 $n$등분하고 각 부분구간에서 최솟값과 최댓값을 높이로 하는 계단함수의 적분을 구한다.

```{code-cell} ipython3
# [7회] 상합과 하합
# 목적 : 정의에 따라 계산한다
# 층  : 1층
# 주의 : n 을 무한대로 보내지 않는다. 모든 n 이 유한하다

머리 = ["n", "하합 L_n", "상합 U_n", "차이 U_n − L_n", "b³/n"]
폭 = [7, 16, 16, 18, 16]
print(" ".join(칸(글, w) for 글, w in zip(머리, 폭)))

for n_ in (2, 5, 10, 50, 200, 1000):
    하, 상 = 상합하합(f, a, b, n_)
    줄 = [n_, f"{하:.10f}", f"{상:.10f}", f"{상 - 하:.10f}", f"{b**3 / n_:.10f}"]
    print(" ".join(칸(항목, w) for 항목, w in zip(줄, 폭)))
```

차이가 (7.17)이 말하는 $b^3/n$과 정확히 일치한다. $n=10$에서 $L_{10}=0.285$, $U_{10}=0.385$이며 이는 1회에서 계산한 값과 같다.

## 실험 2 (2층) — scipy로 대조한다

우리가 정의에 따라 계산한 값이 맞는지 독립적으로 확인한다.

```{code-cell} ipython3
# [7회] scipy 의 수치 구적법으로 대조한다
# 목적 : 우리가 정의에 따라 계산한 값이 맞는지 독립적으로 확인한다
# 층  : 2층
# 주의 : quad 는 구간을 나누어 함숫값을 표본으로 더하는 방법이다.
#        우리가 정의한 것과 같은 계열의 계산이며, 부정적분을 쓰지 않는다.

from scipy.integrate import quad

값, 오차 = quad(lambda t: t**2, a, b)
print(f"scipy.integrate.quad 의 값 : {값:.12f}")
print(f"quad 가 보고한 오차 한계   : {오차:.3e}")

하, 상 = 상합하합(f, a, b, 1000)
print(f"\n우리 계산 (n=1000)")
print(f"  하합 {하:.12f}")
print(f"  상합 {상:.12f}")
print(f"  quad 의 값이 두 값 사이에 있는가 : {하 < 값 < 상}")
```

```{admonition} quad 는 되고 integrate 는 안 되는 이유
:class: warning

`scipy.integrate.quad`는 구간을 나누어 함숫값을 표본으로 더한다. 우리가 {prf:ref}`def-7-integral`에서 정의한 것과 같은 계열의 계산이다. 그래서 이 강의에서 쓴다.

반면 `sympy.integrate`는 다른 경로로 답을 낸다. 이 함수는 먼저 **도함수가 $f$가 되는 함수를 찾고**, 그 함수의 양 끝값의 차를 구한다. 이 강의에서는 이 방법을 쓰지 않는다. 도함수를 아직 정의하지 않았기 때문이다.

두 함수가 같은 답을 준다는 것과 같은 경로로 온다는 것은 다른 이야기이다.
```

## 실험 3 (3층) — 정확한 식

수치는 $L_{1000} = 0.3328335$라고 알려 주지만, 그 값이 $1/3$에서 정확히 얼마나 떨어져 있는지는 말해 주지 않는다. 기호로 계산하면 7회 (7.15)의 식이 그대로 나온다.

```{code-cell} ipython3
# [7회] 상합과 하합의 정확한 식
# 목적 : n 에 대한 식으로 두 값을 쓰고, 오차의 형태를 확인한다
# 층  : 3층
# 금지 : sp.integrate 와 sp.limit 을 쓰지 않는다

from calc_sym import 균등분할_상합하합, 보이기, x, n, b as b기호

하합, 상합 = 균등분할_상합하합(x**2, x, 0, b기호, 증가함수=True)
보이기("L_n", sp.expand(하합))
보이기("U_n", sp.expand(상합))
보이기("U_n − L_n", sp.simplify(상합 - 하합))
```

$b=1$로 두고 $1/3$과의 거리를 본다.

```{code-cell} ipython3
# [7회] 적분값과 하합·상합의 거리
# 목적 : 어떤 유한한 n 에서도 하합이 적분값과 같아지지 않음을 식으로 확인한다
# 층  : 3층
# 주의 : 부호 판정은 인수분해한 식의 분자와 분모를 보고 한다

L1 = sp.expand(하합.subs(b기호, 1))
U1 = sp.expand(상합.subs(b기호, 1))

차1 = sp.factor(sp.simplify(sp.Rational(1, 3) - L1))
차2 = sp.factor(sp.simplify(U1 - sp.Rational(1, 3)))

보이기("1/3 − L_n", 차1)
보이기("U_n − 1/3", 차2)

m = sp.Symbol("m", positive=True, integer=True)
print(f"1/3 − L_n 이 n ≥ 1 에서 양수인가 : {sp.ask(sp.Q.positive(차1.subs(n, m)))}")
print(f"U_n − 1/3 이 n ≥ 1 에서 양수인가 : {sp.ask(sp.Q.positive(차2.subs(n, m)))}")
```

두 식의 분모는 $6n^2$으로 양수이고 분자는 각각 $3n-1$과 $3n+1$이므로, $n \ge 1$에서 둘 다 양수이다. 즉 **어떤 유한한 $n$에 대해서도 $L_n \ne 1/3$이고 $U_n \ne 1/3$이다.**

적분값 $1/3$은 하합들의 최소상계이지 어떤 하합의 값이 아니다.

## 실험 4 (1층) — 분할 무관성

{prf:ref}`thm-7-well-defined`을 수치로 확인한다. 같은 계단함수를 두 분할로 계산한다.

```{code-cell} ipython3
# [7회] 분할 무관성
# 목적 : 계단함수의 적분이 어떤 분할로 계산해도 같은 값이 되는지 확인한다
# 층  : 1층
# 주의 : 분할 B 는 분할 A 의 세분이다. 값이 같아야 한다

def 계단함수적분(분할점, 높이들):
    """7회 (7.1) 을 그대로 옮긴다."""
    return float(np.sum(np.asarray(높이들) * np.diff(분할점)))

분할A = np.array([0.0, 0.5, 1.0]);            높이A = [2.0, 5.0]
분할B = np.array([0.0, 0.2, 0.5, 0.8, 1.0]);  높이B = [2.0, 2.0, 5.0, 5.0]

print(f"분할 A ({len(분할A)-1}개 구간) : {계단함수적분(분할A, 높이A):.8f}")
print(f"분할 B ({len(분할B)-1}개 구간) : {계단함수적분(분할B, 높이B):.8f}")
print(f"같은가 : {계단함수적분(분할A, 높이A) == 계단함수적분(분할B, 높이B)}")
```

## 실험 5 (1층) — 무작위 분할

{prf:ref}`def-7-integral`은 $n$등분에 대한 것이 아니라 **모든** 계단함수에 대한 것이다. 등분이 아닌 분할로도 $1/3$을 가둘 수 있는지 확인한다.

```{code-cell} ipython3
# [7회] 무작위 분할로도 같은 결과
# 목적 : 정의가 n 등분에 의존하지 않음을 확인한다
# 층  : 1층
# 주의 : seed 를 고정하므로 결과는 매번 같다

def 임의분할상합하합(f, a, b, n, seed, 표본수=200):
    r = np.random.default_rng(seed)
    내부 = np.sort(r.uniform(a, b, n - 1))
    분할점 = np.concatenate([[a], 내부, [b]])
    최소, 최대 = [], []
    for k in range(len(분할점) - 1):
        표본 = np.linspace(분할점[k], 분할점[k + 1], 표본수)
        최소.append(f(표본).min())
        최대.append(f(표본).max())
    폭 = np.diff(분할점)
    return float(np.sum(np.array(최소) * 폭)), float(np.sum(np.array(최대) * 폭))

for seed in (0, 1, 2):
    하, 상 = 임의분할상합하합(f, a, b, 300, seed)
    print(f"seed {seed} | 하합 {하:.8f}  상합 {상:.8f} | 1/3 이 사이에 있는가 : {하 < 1/3 < 상}")
```

## 실험 6 (1층) — 그림

파란 몸통이 하합 $L_n$이고 그 위의 주황 띠가 차이 $U_n - L_n$이다.

```{code-cell} ipython3
# [7회] 상합과 하합의 그림
# 목적 : 차이에 해당하는 부분이 어디인지 눈으로 확인한다
# 층  : 1층
# 주의 : 세 그림 모두 유한한 n 에 대한 것이다

그림, 축들 = plt.subplots(1, 3, figsize=(13, 4.3))
for 축, n_ in zip(축들, (4, 10, 40)):
    계단함수그리기(축, f, a, b, n_, 위치="위")
    계단함수그리기(축, f, a, b, n_, 위치="아래")
    함수그리기(축, f, a, b)
    하, 상 = 상합하합(f, a, b, n_)
    무대(축, a, b, f"n = {n_}\n하합 {하:.4f} / 상합 {상:.4f}", y범위=(0, 1.1))

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

$n$이 커지면 주황 띠가 얇아진다. 띠 전체의 넓이가 (7.17)의 $b^3/n$이다.

```{admonition} 확인할 것
:class: tip
1. 실험 3의 $L_n$ 식에 $n=1000$을 대입한 값이 실험 1의 수치와 일치하는지 확인하자.
2. 실험 3에서 $1/3 - L_n$의 식을 인수분해하고, $n\ge1$에서 양수인지 부호를 판정하자.
3. 실험 2에서 `quad`의 값이 하합과 상합 사이에 있는지 확인하자.
4. $n$을 아무리 키워도 하합이 $1/3$과 같아지는 일이 있는가. 실험 3의 식에서 판정하자.
```

```{admonition} 네 번째 항목이 이 회차의 요점이다
:class: warning

$$
\frac{1}{3} - L_n = \frac{3n-1}{6n^2}
$$

이며 $n \ge 1$에서 항상 양수이다. 따라서 어떤 $n$에 대해서도 $L_n \ne 1/3$이다.

적분값 $1/3$은 하합들의 **최소상계**로 정의된 것이지 어떤 하합의 값이 아니다. 5회에서 최소상계가 집합에 속하지 않을 수 있다고 한 것이 여기서 실제로 일어난다.

이 구분이 무너지면 적분을 "무한히 잘게 나눈 합"으로 읽게 된다. {prf:ref}`def-7-integral`에는 그런 과정이 없다.
```

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