[Python]楕円内での光の反射を考え、その軌跡を描くプログラムを作る

# はじめに
今回の楕円内での光の反射の軌跡を描くプログラムを作るにあたって、ただそのプログラムを作成する以上に、いかに**数学的**に、楕円内で反射する光の軌道を計算し、プログラムするのかを重視している.

Githubから来た人には、work2の改訂版(コードを短くし、分かりやすくした)の紹介も兼ねているので、**Githubに公開されているコードよりもこっちの記事を見てほしい.**

# 今回使う公式・数式
+ **楕円の公式   $\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$**
+ **楕円上のある座標(m,n)における接線の方程式 $\frac{mx}{a^2}+\frac{ny}{b^2}=1$**
変形させると $y=-\frac{b^2m}{a^2n}x + \frac{b^2}{n}$
+ **楕円と直線の交点の座標**

  **直線：**$y=cx+d$

  $A=(\frac{1}{a^2}+\frac{c^2}{b^2})$
  $B=2(\frac{cd}{b^2})$
  $C=(\frac{d^2}{b^2}-1)$
  $D=B^2-4AC$

  のように、直線と$A,B,C,D$を定義したときに

  $x1=\frac{-B+\sqrt{D}}{2A}$
  $x2=\frac{-B-\sqrt{D}}{2A}$

  **交点のx座標である$(x1,x2)$が求まる.**
# プログラムの作成
#### 使うライブラリはnumpyとmatplotlib
+ まず始めにライブラリをimport
```python
import numpy as np
import matplotlib.pyplot as plt
``` 
# next_position関数を定義
  + next_position関数の引数は(今向いている方角(傾き)、今いる座標、$a$、$b$)
  + 返り値は、次に到達する座標
```python
def next_position(now_direc ,now_position ,a ,b):
  new_position = [None,None]
  c = now_direc
  d = -1 * now_direc * now_position[0] + now_position[1]

  A = (1/a**2 + c**2/b**2)
  B = 2 * c * d / b**2
  C = (d**2 / b**2) -1
  D = B**2 - 4 * A * C

  x1 = (-B + D**(1/2)) / (2*A) #直線と楕円の交点のx座標(x1,x2)
  x2 = (-B - D**(1/2)) / (2*A)
  #今、x1とx2のどちらの座標にいるか分からないので条件分岐させる
  if round(now_position[0],8) == round(x2,8):
  #コンピューター上では、5が5.000001になったりして正しく計算できないのでroundで少数第8桁まで丸め込む
    new_position[0] = x1
    s = (0 - now_position[1])/(a - now_position[0]) #今いる座標と点(x,y) = (a,0)を通る直線の傾き

    if now_direc >= s:
      new_position[1] = (b**2 - (x1**2 * b**2 / a**2)) ** (1/2)
    else:
      new_position[1] = -(b**2 - (x1**2 * b**2 / a**2)) ** (1/2)
  
  else:
    new_position[0] = x2
    s = (0 - now_position[1])/(-a - now_position[0])#今いる座標と点(x,y) = (-a,0)を通る直線の傾き

    if now_direc >= s:
      new_position[1] = -(b**2 - (x2**2 * b**2 / a**2)) ** (1/2)
    else:
      new_position[1] = (b**2 - (x2**2 * b**2 / a**2)) ** (1/2)

  return new_position#次に到達する座標
```
#### next_position関数の説明

```python
if round(now_position[0],8) == round(x2,8):
  new_position[0] = x1
```
最初の分岐で、今、x1とx2のどちらの座標にいるか判断し、今いない方のx座標(次に到達する座標のx座標)をnew_position[0]に代入.

今回の例では上の条件分岐通り、今いる$x$座標は、$x2$とする$(x1 > x2)$
```python
s = (0 - now_position[1])/(a - now_position[0])
if now_direc >= s:
  new_position[1] = (b**2 - (x1**2 * b**2 / a**2)) ** (1/2)
else:
  new_position[1] = -(b**2 - (x1**2 * b**2 / a**2)) ** (1/2)
```
円において、$y$の式は、$y = \pm{\sqrt{(b^2-\frac{b^2x^2}{a^2})}}$の2種類あるため、ここでも条件分岐させる必要がある.

下の図では、<font color="yellow">**黄点**</font>は$x1$に対応する座標の例、<font color="blue">**青点**</font>は$x2$に対応する座標の例としている(この時点では、まだ黄点の座標は分からないが$x1 > x2$ということは分かっている).

変数$s$:$\frac{0-今いるy座標}{a-今いるx座標}$　　は<font color="blue">**青点**</font>から<font color="red">**赤点**</font>に直線(<font color="red">**赤線**</font>)を引いたときの傾きになっている.
そして、現在向いている向き(傾き:now_direc)が<font color="blue">**青線**</font>もしくは<font color="green">**緑線**</font>となる.

![スクリーンショット 2023-01-15 200709.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/3dedc36a-291d-9f11-3f91-752e5367fbeb.jpeg)

現在いる$x$座標が**今回の例のような$x2$だった場合、<font color="blue">**青線**</font>もしくは<font color="green">**緑線**</font>の傾きが<font color="red">**赤線**</font>よりが大きい(now_direc$>=s$)と、必ず次の到達点の$y$座標は0以上になる.**
よって、$y = \sqrt{(b^2-\frac{b^2x^2}{a^2})}$に次に到達する$x$座標を代入すると、次に到達する$y$座標が出力される.

現在いる座標が$x1$だった場合は、これと逆になる.
***
# reflect_direc関数を定義
  + 引数は(次に到達する座標、現在向いている方角(傾き)、a、b)
  + 返り値は、次に到達する座標に到達したときに反射する向き(傾き)

```python
def reflect_direc(new_position,now_direc,a,b):

  #tan_degは次到達する座標の接線の傾きの角度(-90<=tan_deg<=90)
  if new_position[1] == 0:
    tan_deg = 90
  else:
    deg = -b**2 * new_position[0]/(a**2 * new_position[1])
    tan_deg = np.degrees(np.arctan(deg))

  direc_deg = np.degrees(np.arctan(now_direc))#now_direcを角度に変換(-90<=direc_deg<=90)
  theta = 2*tan_deg - direc_deg

  return_direc = np.tan(np.radians(theta))
  
  return return_direc
```
#### reflect_direc関数の説明
```python
 if new_position[0] == 0:
    tan_deg = 90        
 else:
    deg = -b**2 * new_position[0]/(a**2 * new_position[1])
    tan_deg = np.degrees(np.arctan(deg))

  direc_deg = np.degrees(np.arctan(now_direc))
```
楕円上のある座標における接線の傾きは、上の公式、数式一覧で紹介している通り$-\frac{b^2m}{a^2n}$だが、**分母に0を取ることができない.**

new_position[1]が$n$にあたるため、new_position[1] == 0の場合、すなわち次に到達する座標が$(a,0)$か$(-a,0)$のとき、**あらかじめtan_deg = 90と設定しておく必要がある.**

direc_degで現在向ている方角(傾き:now_direc)を角度に変換(-90<=direc_deg<=90)
```python
  theta = 2*tan_deg - direc_deg

  return_direc = np.tan(np.radians(theta))
  
  return return_direc
```
**thetaは反射した後の光の進む方向($x$軸に対する角度)となっている.**

:::note warn
注意
角度は-90~90で定義されています.
:::

![qiita.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/030969e0-51a9-3d93-ce9d-3acd6ee84823.jpeg)

上の図で、<font color = 'green'>**反射板(緑線)**</font>に対する<font color = 'red'>**入射光(赤線)**</font>の角度(<font color = 'red'>direc_deg</font> - <font color = 'green'>tan_deg</font>)は、<font color = 'purple'>**角度紫**</font>(反射板に対する入射角の角度なので、上の例の場合は負)となっていることが分かる.

よって、「<font color = 'green'>**反射板(緑線)**</font>に対する**反射光**の**反射角度**」は、「<font color = 'green'>**反射板(緑線)**</font>に対する<font color = 'red'>**入射光(赤線)**</font>の<font color = 'red'>**入射角度(角度赤)**</font>をプラスマイナス反転したもの」で、<font color = 'green'>tan_deg</font>-<font color = 'red'>direc_deg</font>となる.

その「<font color = 'green'>**反射板(緑線)**</font>に対する**反射光**の**反射角度**(<font color = 'green'>tan_deg</font>-<font color = 'red'>direc_deg</font>)」を<font color= 'green'>**角度緑**</font>(x軸に対する角度なので、今回の例の場合は負)だけ時計回り方向に回転させる((<font color = 'green'>tan_deg</font> - <font color = 'red'>direc_deg</font>) + <font color = 'green'>tan_deg</font>)と、<font color = 'black'>**角度黒**</font>(x軸に対する反射光の角度(2*<font color = 'green'>tan_deg</font> - <font color = 'red'>direc_deg</font>))が出てくることが分かる.

![qiita2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/d7f17e56-52f3-067d-3343-0e27189a7525.jpeg)

つまり上の図のように、まず<font color = 'green'>**反射板(緑線)**</font>に対する<font color ='black'>**反射光(黒線)**</font>の反射角度を求めて、そしてその反射板を<font color ='orange'>**任意の角度(橙角度)**</font>だけ回転するのを想像すればいい.
+ 上の例では、楕円内の第3象限の部分で、光が反射しているときの例を紹介しているが、その場合は<font color = 'green'>**反射板**</font>に対する**反射光**の反射角度にtan_deg(<font color = 'orange'>**橙角度**</font>)を足す.

+ 第1象限での反射の場合は、180 + tan_degだけ足す(傾き(tan(角度))を求めたいので180は消すことができる).
+ 第2象限での反射の場合は、180-tan_degだけ引く(傾き(tan(角度))を求めたいので180は消すことができる).
+ 第4象限での反射の場合は、tan_degだけ足す.

**よって、反射光の反射角(x軸に対する角度)は2*tan_deg - direc_degとなる.**

最後に、return_direcで、角度を傾きに変換してそれを関数の返り値とする.

# 最後にプロットする

#### 入力値は、最初に光を放つ角度、何回反射させるか、楕円のx方向の大きさ、y方向の大きさ
```python
now_direc = np.tan(np.radians(int(input('打ち出す角度を設定してください(0から180):'))))
h = int(input('何回光を反射させるか決めてください：'))
a = int(input('楕円のx軸方向の長さを設定してください：'))
b = int(input('楕円のy軸方向の長さを設定してください：'))

first_posi = [0,-b]#光を放つ初期位置を設定.楕円の真ん中下から放つ.
next = next_position(now_direc,first_posi,a,b)
direc = reflect_direc(next,now_direc,a,b)#次の場所に向かうための方向を計算

posi = np.array([[0],[-b]])
for _ in range(0,h): #h回反射させます。
  posi = np.append(posi,np.array([[next[0]],[next[1]]]),axis = 1)
  next = next_position(direc,next,a,b)
  direc = reflect_direc(next,direc,a,b)

t = np.linspace(-a,a,a*100)#楕円の作成
y = (b**2 - (t**2 * b**2 / a**2)) ** (1/2)
y1 = -1 * (b**2 - (t**2 * b**2 / a**2)) ** (1/2)

plt.plot(t,y,c = 'b')#楕円をプロット
plt.plot(t,y1,c = 'b')#反射する光の軌跡をプロット

plt.grid()
plt.plot(posi[0,:],posi[1,:])
plt.axis('equal')
plt.show()
```
## 結果
80度、60回、$a = 1,b = 1$だと
![11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/b349a467-5bf2-0463-ea0e-55ab41d74066.jpeg)
となる。
$a = 3,b = 2$に変えてみると、上と全然違う形になる.
![32.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/321d46eb-14d8-6a93-bc7b-9db17f09d778.jpeg)
$a = 4,b = 9$の場合も形が全然違う.
![49.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920569/cc9f95e5-e064-eab6-a09c-535181484fa4.jpeg)

# 終わりに
放つ角度が同じでも、aとbを変えるだけでここまで出力される軌跡が違うのは、**楕円の面白いところ**ですね.

また今度このコードを使って、進化版ビリヤードのようなものや、放つ角度を少しずつ変えていった時に、反射する光の軌跡がどのように変化していくかを見れるプログラムも書きたいと思います.

この記事が、楕円内に放たれた光の反射をどのように考え、プログラムするかを知りたい人の手助けになれれば嬉しいです.