sagantaf

機械学習やITインフラ、特にコンテナ関連の記事を書いているブログ。

ケプラー方程式をgo言語を使って反復計算法で解く

背景

go言語と宇宙工学の勉強のために、ケプラーの方程式を反復計算法(ニュートン ラフソン法)で解いてみました。

定数の設定

// 地球の重力定数
const gravitationalConstant = 398600.4

平均近点離角Mを計算する関数

func MeanAnomaly(a, deltaT float64)float64{
    return math.Sqrt(gravitationalConstant/math.Pow(a, 3)) * deltaT
}

aは、半長軸(km)です。

また、deltaTは 元期 から求めたい瞬間の時刻の経過時間(秒)になります。

この関数によって平均点離角(radian)をreturnしています。

ケプラー方程式

ケプラー方程式は

離心近点離角をE、平均近点離角をM、離心率e とした時に

$$ M = E - e sinE $$

で表される方程式です。

この方程式は普通の計算では解けず、ニュートン ラフソン法のような反復方程式などによって解けるらしいです。

さて、この方程式を、

$$ F(E) = E - e sinE -M $$

と定義すると、

$$ E_{n+1} = E_n + \frac{F(E_n)}{1 - e cosE_n} $$

と書くことができ、反復計算ができる形になります。(細かい導出方法はすっ飛ばしてます。参考文献の本に詳細に記述されています。)

この式をクロージャとして定義し、後ほど反復法の関数にて利用していきます。

// ケプラー方程式
func KeplerEquation(e, M float64) func(Ebefore float64) float64{
    // Ebefore: n回目の離心近点離角
    // M: 平均近点離角
    // e: 離心率
    // Eafter: n+1回目の離心近点離角

    return func(Ebefore float64)float64{
        FE := Ebefore - e*math.Sin(Ebefore) - M
        Eafter := Ebefore - FE/(1-e*math.Cos(Ebefore))
        return Eafter
    }
}

反復計算法(ニュートン ラフソン法)

許容誤差を引数として指定し、計算誤差が許容誤差よりも小さくなったら、forループを抜け、計算結果をreturnするようにしています。

// ケプラー方程式をニュートン-ラフソン法で解く
func NewtonRaphson(e, before, ae float64) float64 {
    // M: 平均近点離角
    // e: 離心率
    // ae: allowable error 許容誤差

    equation := KeplerEquation(e, before)

    var after float64
    err := 100.0 // 誤差の初期化(0だとforが回らないため)
    count := 0

    for ; err > ae; {
        after = equation(before)
        err = math.Abs(after - before)
        fmt.Println("誤差: ", err)

        before = after
        count = count + 1
        fmt.Println("繰り返し:", count, "回目")
        fmt.Println("----")
    }
    return after
}

main関数

ここまでで、必要な関数の定義は終わりです。

あとはmain関数で実際に使ってみて、正しい計算結果が得られるか確認してみます。

半長軸aを2000km、経過時間deltaTを3600秒、離心率eを0.75、許容誤差を0.0001として計算してみます。

func main() {
    fmt.Println("ケプラー方程式を使って離心近点離角Eを求めます:")
    M := MeanAnomaly(2000, 3600)
    Mdigree := int(M * 180 / math.Pi) % 360
    E := NewtonRaphson(0.75, M, 0.0001)
    Edigree := int(E * 180 / math.Pi) % 360
 
    Mrad := math.Round(M*100)/100
    Erad := math.Round(E*100)/100
    fmt.Printf("\n平均近点離角Mは %vrad、%v° です。", Mrad, Mdigree)
    fmt.Printf("\n離心近点離角Eは %vrad、%v° です。", Erad, Edigree)
}

出力結果は

ケプラー方程式を使って離心近点離角Eを求めます:
誤差:  0.7393437287609679
繰り返し: 1 回目
----
誤差:  0.1667982273838291
繰り返し: 2 回目
----
誤差:  0.016917352484746573
繰り返し: 3 回目
----
誤差:  0.00016185780524580196
繰り返し: 4 回目
----
誤差:  1.4669140568912553e-08
繰り返し: 5 回目
----

平均近点離角Mは 25.41rad、15° です。
離心近点離角Eは 25.97rad、47° です。

となります。

下記のサイトの結果と比較してみると、少しの計算誤差はあるものの、ほぼ正しく計算できていることが分かりました!

ケプラー方程式の解 - 高精度計算サイト

コード全体

コード全体を残しておきます。

package main

import (
    "fmt"
    "math"
)

// 地球の重力定数
const gravitationalConstant = 398600.4

// 平均近点離角
func MeanAnomaly(a, deltaT float64)float64{
    return math.Sqrt(gravitationalConstant/math.Pow(a, 3)) * deltaT
}

// ケプラー方程式
func KeplerEquation(e, M float64) func(Ebefore float64) float64{
    // Ebefore: n回目の離心近点離角
    // M: 平均近点離角
    // e: 離心率
    // Eafter: n+1回目の離心近点離角

    return func(Ebefore float64)float64{
        FE := Ebefore - e*math.Sin(Ebefore) - M
        Eafter := Ebefore - FE/(1-e*math.Cos(Ebefore))
        return Eafter
    }
}

// ケプラー方程式をニュートン-ラフソン法で解く
func NewtonRaphson(e, before, a float64) float64 {
    // M: 平均近点離角
    // e: 離心率
    // a: allowable error 許容誤差

    equation := KeplerEquation(e, before)

    var after float64
    err := 100.0 // 許容誤差の初期化(0だとforが回らないため)
    count := 0

    for ; err > a; {
        after = equation(before)
        err = math.Abs(after - before)
        fmt.Println("誤差: ", err)

        before = after
        count = count + 1
        fmt.Println("繰り返し:", count, "回目")
        fmt.Println("----")
    }
    return after
}




func main() {
    fmt.Println("ケプラー方程式を使って離心近点離角Eを求めます:")
    M := MeanAnomaly(2000, 3600)
    Mdigree := int(M * 180 / math.Pi) % 360
    E := NewtonRaphson(0.75, M, 0.0001)
    Edigree := int(E * 180 / math.Pi) % 360
 
    Mrad := math.Round(M*100)/100
    Erad := math.Round(E*100)/100
    fmt.Printf("\n平均近点離角Mは %vrad、%v° です。", Mrad, Mdigree)
    fmt.Printf("\n離心近点離角Eは %vrad、%v° です。", Erad, Edigree)
}

参考文献

以下の本で理論を参考にしました。

惑星探査機の軌道計算入門

惑星探査機の軌道計算入門

  • 作者:半揚 稔雄
  • 発売日: 2017/09/20
  • メディア: 単行本(ソフトカバー)

ミッション解析と軌道設計の基礎

ミッション解析と軌道設計の基礎

円錐曲線(楕円、双曲線、放物線)のグラフをgo言語で描く

背景

go言語と宇宙工学の勉強のために、円錐曲線のグラフを描いてみました。

円錐曲線の式で一気に、放物線も双曲線も楕円も描けます。

$$ r = \frac{l}{1 + e cos\theta} $$

離心率eと半直弦lを変えることで、放物線、双曲線、楕円、円になります。

main関数

まずは、イメージを湧かせるためにmain()関数から記載します。

func main(){
    // データ取得
    thetaList := GetThetaData()

    // パラメタ指定
    e := 0.5 // 離心率
    l := 1.5 // 半直弦
    graphTitle = "parabola polar"
    savePath = "/tmp/parabola-polar.png"

    // 描画実行
    DrawConic(e, l, thetaList, graphTitle, savePath)
}

流れとしては、thetaの数値リストを取得し、パラメタを指定した後に、描画用の関数を実行しています。

thetaデータの生成

ここでは単純に0° ~ 360°の時のthetaを10°ごとに計算し、リストにしているだけです。

func GetThetaData() (thetaData []float64){
    for angle := 0.0; angle <= 360; {
        theta := angle * math.Pi / 180
        thetaData = append(thetaData, theta)
        angle = angle + 10
    }
    return
}

DrawConic()関数

円錐曲線を計算するクロージャCalcConicPolar()(後述)を設定後、データ1つ1つに対して適用していきます。

また、極座標を直交座標に変換しています。

最後に自作関数plotDrawLiner()(後述)にて描画とpng保存を実行します。

func DrawConic(e, l float64, thetaList []float64, graphTitle, savePath string){
    // クロージャを設定
    c := CalcConicPolar(e, l)

    // radius算出
    var radiusList []float64
    for _, v := range thetaList{
        r := c(v)
        radiusList = append(radiusList, r)
    }

    // 座標変換
    var xys [][]float64
    for i, theta := range thetaList{
        radius := radiusList[i]
        xy := make([]float64, 2)
        x, y := Polar2cartesian(radius, theta)
        xy[0] = math.Round(x*100) / 100
        xy[1] = math.Round(y*100) / 100
        xys = append(xys, xy)
    }

    // 描画&保存
    PlotDrawLiner(graphTitle, savePath, xys)
}

クロージャ CalcConicPolar()関数

極座標による円錐曲線を計算するためのクロージャ(極座標θ, r)です。

離心率: e, 半直弦: l をクロージャの引数として受け取るようになっています。

このクロージャを呼び出すごとにthetaを渡すことで、radiusが返されます。

func CalcConicPolar(e, l float64) func(theta float64) float64 {
    return func(theta float64) float64 {
        return l / (1 + e * math.Cos(theta)) // 冒頭の円錐曲線の式より
    }
}

座標変換(極座標→直交座標)Polar2cartesian()関数

グラフにプロットできるようにするために、極座標r, θから直交座標x, yへ変換する関数をつくります。

func Polar2cartesian(r, theta float64)(x, y float64){
    x = r * math.Cos(theta)
    y = r * math.Sin(theta)
    return
}

グラフの描画と保存 PlotDrawLiner()関数

グラフの描画には、

GitHub - gonum/plot: A repository for plotting and visualizing data

を利用しました。

func PlotDrawLiner(graphTitle, savePath string, data [][]float64){
    // 図の作成
    p, err := plot.New()
    if err != nil{
        log.Fatalln("error: ", err)
    }

    // グラフの装飾の設定
    p.Title.Text = graphTitle
    p.X.Label.Text = "X"
    p.Y.Label.Text = "Y"
    p.Add(plotter.NewGrid()) // 補助線

    // 描画するデータの取得
    pts := make(plotter.XYs, len(data))
    var xDataList []float64
    var yDataList []float64

    for i, pair := range data {
        pts[i].X = pair[0]
        xDataList = append(xDataList, pair[0])
        pts[i].Y = pair[1]
        yDataList = append(yDataList, pair[1])
    }

    // 折れ線グラフの描画
    err = plotutil.AddLinePoints(p, pts)
    if err != nil {
        log.Fatalln("failed to draw liner: ", err)
    }

    // 座標範囲の設定
    xMax, xMin, err := GetMaxMin(xDataList)
    if err != nil {
        log.Fatalln("failed to get max and min from xDataList: ", err)
    }
    yMax, yMin, err := GetMaxMin(yDataList)
    if err != nil {
        log.Fatalln("failed to get max and min from yDataList: ", err)
    }
    p.X.Min = xMin - 1
    p.X.Max = xMax + 1
    p.Y.Min = yMin - 1
    p.Y.Max = yMax + 1

    // 保存
    if err := p.Save(6*vg.Inch, 6*vg.Inch, savePath); err != nil {
        log.Fatalln("Failed to save plot:", err)
    }
}

最大値と最小値を取得 GetMaxMin()関数

与えられたリストの中で最大値と最小値を返す関数です。

標準ライブラリには、二つの数値を比較する関数しか存在しなかったため、自作しました。

ちなみにその標準ライブラリを中で利用しています。(math.Max, math.Min)

func GetMaxMin(inputList []float64)(max, min float64, err error){
    if len(inputList) == 0 {
        return 0, 0, errors.New("input list is empty")
    }
    max = inputList[0]
    min = inputList[0]
    for _, p := range inputList{
        max = math.Max(max, p)
        min = math.Min(min, p)
    }
    err = nil
    return
}

コード全体

最後にここまでのコード全体を記載します。

package main

import (
    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/plotutil"
    "gonum.org/v1/plot/vg"
    "log"
    "math"
)

func GetThetaData() (thetaData []float64){
    for angle := 0.0; angle <= 360; {
        theta := angle * math.Pi / 180
        thetaData = append(thetaData, theta)
        angle = angle + 10
    }
    return
}

func CalcConicPolar(e, l float64) func(theta float64) float64 {
    return func(theta float64) float64 {
        return l / (1 + e * math.Cos(theta)) // 冒頭の円錐曲線の式より
    }
}

func PlotDrawLiner(graphTitle, savePath string, data [][]float64){
    // 図の作成
    p, err := plot.New()
    if err != nil{
        log.Fatalln("error: ", err)
    }

    // グラフの装飾の設定
    p.Title.Text = graphTitle
    p.X.Label.Text = "X"
    p.Y.Label.Text = "Y"
    p.Add(plotter.NewGrid()) // 補助線

    // 描画するデータの取得
    pts := make(plotter.XYs, len(data))
    var xDataList []float64
    var yDataList []float64

    for i, pair := range data {
        pts[i].X = pair[0]
        xDataList = append(xDataList, pair[0])
        pts[i].Y = pair[1]
        yDataList = append(yDataList, pair[1])
    }

    // 折れ線グラフの描画
    err = plotutil.AddLinePoints(p, pts)
    if err != nil {
        log.Fatalln("failed to draw liner: ", err)
    }

    // 座標範囲の設定
    xMax, xMin, err := GetMaxMin(xDataList) // common.goの関数
    if err != nil {
        log.Fatalln("failed to get max and min from xDataList: ", err)
    }
    yMax, yMin, err := GetMaxMin(yDataList) // common.goの関数
    if err != nil {
        log.Fatalln("failed to get max and min from yDataList: ", err)
    }
    p.X.Min = xMin - 1
    p.X.Max = xMax + 1
    p.Y.Min = yMin - 1
    p.Y.Max = yMax + 1

    // 保存
    if err := p.Save(6*vg.Inch, 6*vg.Inch, savePath); err != nil {
        log.Fatalln("Failed to save plot:", err)
    }
}


func DrawConic(e, l float64, thetaList []float64, graphTitle, savePath string){
    c := CalcConicPolar(e, l)

    // radius算出
    var radiusList []float64
    for _, v := range thetaList{
        r := c(v)
        radiusList = append(radiusList, r)
    }

    // 座標変換
    var xys [][]float64
    for i, theta := range thetaList{
        radius := radiusList[i]
        xy := make([]float64, 2)
        x, y := Polar2cartesian(radius, theta)
        xy[0] = math.Round(x*100) / 100
        xy[1] = math.Round(y*100) / 100
        xys = append(xys, xy)
    }

    // 描画&保存
    PlotDrawLiner(graphTitle, savePath, xys)
}


func main(){
    // データ取得
    thetaList := GetThetaData()

    // 楕円
    e := 0.5 // 離心率
    l := 1.5 // 半直弦
    graphTitle = "parabola polar"
    savePath = "/tmp/parabola-polar.png"

    // 描画実行
    DrawConic(e, l, thetaList, graphTitle, savePath)
}

結果

これを実行することで、以下のようなpngファイルを取得できます。

f:id:sagantaf:20200824004134p:plain
楕円

また、e=1.1など、離心率eを1以上にすることで、双曲線が描けます。

f:id:sagantaf:20200824004104p:plain
双曲線

Go言語 - 指定した範囲の数値をintervalごとにリストにして返す

背景

指定した範囲でランダム値を返す方法はググれば見つかったけど、intervalごとに数値を生成する方法は見つからなかったので書いてみました。

中身

CreateNumList関数として作成。入力値も出力値もfloat64のtypeにしています。

// init ~ last の数値をintervalごとにリストにして返す
func CreateNumList(init, last, interval float64)[]float64{
    var nums []float64
    for i := init; i < last; {
        nums = append(nums, i)
        i = math.Round((i + interval)*100) / 100   // ※
    }
    return nums
}

引数として、initとlast、intervalを取得します。init以上last未満の値がintervalで指定した間隔でリストとして格納されます。

例えば

fmt.Println(CreateNumList(1,30,3))

とすると、

[1 4 7 10 13 16 19 22 25 28]

という出力を得られます。

※印の部分では、iを四捨五入してから更新しています。四捨五入をしないでi = i + intervalとすると、微小な計算誤差により、桁数の大きい数値が格納されてしまいます。

例えば、

CreateNumList(1,2,0.2)

としたときに、四捨五入をしないコードで実行すると、

[1 1.2 1.4 1.5999999999999999 1.7999999999999998 1.9999999999999998]

と表示されてしまいます。そのため、math.Round()を使って四捨五入しています。

ただし、math.Roundは整数になるまで四捨五入してしまうため、ここでは先に100倍して後から100で割ることで、小数点第二位での四捨五入を実現しています。

無限ループに注意

ここで注意点があります。

今回は、小数点第二位で四捨五入してから iの更新をしているため、もしintervalが0.001など小数点第二位よりも小さい値だった場合、iは更新されず無限ループになります。 (ここの判定ロジックを組むのは手間だったのでそのままにしています…)

まとめ

利用例とともにコード全体を記しておきます。

package main

import (
    "math"
    "fmt"
)

func CreateNumList(init, last, interval float64)[]float64{
    var nums []float64
    for i := init; i < last; {
        nums = append(nums, i)
        i = math.Round((i + interval)*100) / 100
    }
    return nums
}

func main(){
    fmt.Println(CreateNumList(1,30,3))
    fmt.Println(CreateNumList(10,12,0.2))
    fmt.Println(CreateNumList(1,1,1))
    // fmt.Println(calcSat.CreateNumList(1,2,0.005)) // 無限ループ
}

このコードの出力は

[1 4 7 10 13 16 19 22 25 28]
[10 10.2 10.4 10.6 10.8 11 11.2 11.4 11.6 11.8]
[]

です。

アイデアのつくり方のまとめ

書籍「アイデアのつくり方」のまとめです。

アイデアのつくり方

アイデアのつくり方

中身は、書籍のままであることが多いですが、自分なりの言葉に変換し、実際に使える形にまとめてみました。文章をそのまま引用した部分は引用符をつけています。

イデアをつくるための基本的な流れ

イデアをつくる、閃くために必要なステップは以下の5つです。この詳細が本に記載されています。

  1. 得たいアイデアに関する情報を大量にインプットをする
  2. インプットした結果、得た情報をいろいろ組み合わせたり、妄想したりしてイデアを発散させる
  3. 一度完全に忘れて、寝かせて、閃くのを待つ
  4. 閃く
  5. 一気に具体化させて検証する

本のまとめ

原理と方法

どんな技術を習得する場合にも、学ぶべき大切なことはまず第一に原理であり、第二に方法である。これはアイデアを作り出す技術についても同じことである。

イデアをつくるためには、

イデアの源泉にある原理を把握する

ことが必要。この原理を把握することで、新しいアイデアを閃くことができる。

ではその原理を把握するにはどうするか。

イデアとは既存の要素の新しい組み合わせ以外の何ものでもない

つまり既存の要素/事物に潜んでいる関連性を見つけ出すことで、そこから一つの原理を引き出すことができる。

しかし、この関連性を見つけ出すには、それなりの訓練が必要になる。

この習性を訓練する良い方法は、社会科学の勉強をやること。

社会科学を学びながら、事物と事物の関連性を探る能力を身につけられる。

イデアを作り出す5つの段階

ここからが本題。

事物と事物の関連性を見つけ出し、原理を理解し、新しいアイデアをつくるために必要な具体的な方法として、5つの段階を経る必要がある。


1. 資料を収集する

一番大切な、一番無視されがちな段階。

集める資料は、特殊資料と一般資料の2種類。

特殊資料は、ビジネスの場合、製品とその商品を売りたい人々についての資料を指す。資料と言っても紙やネットにある情報だけではなく、自分の手足を動かして、実証した情報も指している。(人にヒアリングする、実際に自分が体験する、社会を観察する、など)

特殊知識はいわゆる業界特有の知識とも言えるかもしれない。

一般的資料は、世の中の様々な出来事を指している。あらゆる方面のどんな知識でもむさぼり食うくらいの興味が必要。

資料を探す作業は、根気が必要であるため、大抵の人は早めにやめてしまう。表面的な部分だけを捉えて、そこには何も相違がないと決めてしまう。しかし、十分深く掘り下げていくと、全ての製品と消費者の間に、関係の特殊性が見つかる。

この、特殊「知識」と一般的「知識」との新しい組み合わせから、アイデアが生まれてくる。


2. 集めた情報を咀嚼する

集めてきた情報を様々な角度から考えてみる。

一つの事物を取り上げ、視点(自分、顧客、友人、他人など)や視野(具体化、抽象化)を変えて観察してみる。 また、一般的知識と特殊知識を並べて類似点や相違点を探してみる。

そうすることで事物の本質や関係を見つけることができる。

何度も繰り返し、組み合わせを考え続ける。もう本当にこれ以上組み合わせが存在しないと感じ、それでも何もはっきりと閃いておらず絶望状態になったら、第二段階は完了になる。


3. 完全に忘れる

ここまできたら、一度問題を完全に放棄する。できるだけ心の外に放り出して、別のことに集中する。


4. 閃く

完全に問題を放棄してしばらく経ったら、突然新しいアイデアが閃く。この瞬間が第四段階であり、すぐ次の第五段階に移る。


5. アイデアを練り込む

閃いたアイデアを現実の世界で検証してみる。どうすれば実現できるかを徹底的に考える。しかし、ほとんど全てのアイデアが社会ではやっていけないアイデアであることに気づく。そこで諦めてしまわずに、様々な手を考え、仮説検証する必要がある。忍耐や想像力、仮説力が問われる場面となる。

イデアつくりは難しいが誰にでもできる

この本の冒頭には、

説明は簡単至極だが実際にこれを実行するとなると最も困難な種類の知能労働が必要なので、この公式を手に入れたと言っても、誰もがこれを使いこなすというわけにはいかないということである。

と記載されています。

確かに読み通して、以下のような能力が必要になると感じました。

  • あらゆることにアンテナを張って情報を収集する能力
  • イデアをつくりたい業界の専門的知識を理解する能力
  • 事物を抽象化して、比較したり、組み合わせる能力
  • ひたすら考え続ける集中力
  • 諦めないメンタル

これだけの能力が揃って初めて世の中に送り出せる立派なアイデアが生まれるということで、なんとなくの閃きから来るものではないことがよくわかりました。

逆に、アイデアをつくる、というのは先天的な才能でも何でもなく、しっかりと前述の能力を身につけることで、誰でもアイデアをつくり、世に出せるということです。

最後に

この本は、1988年に初版が発行されおり、かなり古いため、文章が読みにくいと感じる方もいるのではないかと思いました。そんな方々の読む前や読んだ後の一助になれば幸いです。

GoLandをMacにインストールし起動する(無料版)

環境

  • インストール対象のOS: MacOS Catalina
  • インストールするGolandのバージョン:GoLand 2020.1.4

ダウンロード

GoLandは以下のページからダウンロードできます。30日間無料体験できるので、まずは試してみようと思います。

www.jetbrains.com

ダウンロード後、パッケージを開くと f:id:sagantaf:20200726213530p:plain

という画面が出るので、ApplicationsにGoLandのアイコンをドラッグして持っていきます。

完了の知らせなどは何も出ずに静かにインストールが完了するので、アプリ一覧からGoLandを起動させます。


初期設定

初回起動時はMacの警告が出るため、開くを選択し、起動させます。 f:id:sagantaf:20200726213617p:plain

開くと最初にAgreementやData Shareのお知らせが出ますので、適当に進めていきます。

次にUIテーマの選択画面が表示されますので、好きな方を選択します。 f:id:sagantaf:20200726213647p:plain

UIテーマを選択後は、起動時に動かせるスクリプトファイルを指定する設定画面が出ます。後からでも設定できるため、ここは何もせず、次にいきます。 f:id:sagantaf:20200726213911p:plain

最後にプラグインの追加ができますが、ここも後から設定できるため、飛ばします。 f:id:sagantaf:20200726213932p:plain

Start using Golandを押すと、次はライセンス有効化の画面に遷移します。

f:id:sagantaf:20200726213952p:plain無償お試ししたいので、Evaluate for freeを選択し、Evaluateを押します。

起動

ここでようやくGoLandが起動し、Tutorialsを進めたり、Projectが作れるようになります。 f:id:sagantaf:20200726214011p:plain

Tutorialsを進めることで自然と使い方に慣れることができたので、一度実施することをお勧めします。

費用

購入する場合は、法人と個人、月額と年額で金額が異なります。

個人かつ月額の場合は、¥1,030/月です。 詳しくは以下のページから確認できます。

購入 GoLand:価格とライセンス、割引 - JetBrains Toolboxサブスクリプション

参考

Golandの日本語ドキュメントは以下のサイトにあります。

pleiades.io