Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

Box2Dベンチマーク (Python 線分交差判定)

先日、線分交差判定の方法をまとめました。こういった当たり判定を多用する物理エンジンでは、Sweep and Pruneアルゴリズムなどを用いて当たり判定の枝刈りをしたり、そもそもネイティブPythonではなく、C++で実装して高速化するなどしています。

そういった2D向け物理エンジンでPython向けのインタフェースがあるものにBox2DやChipmunkがありますが、OpenAI GymのBipedalWalkerではBox2Dが採用されています。このBox2Dがネイティブ実装と比較してどのくらいの性能が出るのか比較しました。

Sweep and Pruneアルゴリズムの概念図

実行環境

  • python 3.8
  • Box2D==2.3.10
  • numba==0.54.1

ベンチ環境

大量の隔壁に向けてLidarを照射し、距離を測ります。ネイティブ実装では純粋に"壁の数×レーザー数"回だけの当たり判定処理を行います。アルゴリズムについては先日のエントリーをご参照ください。

ベンチマーク環境

結果

ノートをこちらで公開

縦10x横1000枚の隔壁にレーザーを照射した結果がこちらで、Box2Dが圧倒的でした。Pythonインタフェースのドキュメントが充実していないので使うのがしんどいですが、今度から手組み辞めようかなと思うくらい速い…。

  方法 結果[msec] 指数
1 Box2D 0.037 1.0
2 Numba実装 202.878 5,483.0
3 Native Python実装 549.742 14,857.9

そして、試した範囲内では理論通りO(n)になっているっぽい(※ Box2Dのグラフは細かく読んでない)

実行結果

Box2D実装メモ

Box2D Pythonインタフェースのドキュメントは体系的なものがないため毎度困るので実装メモを残す。

シミュレーション世界の初期化

import Box2D
from Box2D.b2 import (edgeShape, fixtureDef)

world = Box2D.b2World()
# v2.1.2でedgeShapeが導入された[1]
walls_2d = [
  fixtureDef(shape = edgeShape(
    vertices= [(w[0][0], w[0][1]), (w[1][0], w[1][1])])
  ) for w in walls
]
world.CreateStaticBody(fixtures = walls_2d)
# 上記とこれに大きな違いはない
# for f in walls_2d:
#   world.CreateStaticBody(fixtures = f)
class LidarCallback(Box2D.b2.rayCastCallback):
    # BipedalWalkerでは基底クラスに含まれている扱いだが明示的に宣言必要
    p1 = [0., 0.]
    p2 = [0., 0.]
    fraction = 1.0
    def ReportFixture(self, fixture, point, normal, fraction):
        self.p2 = point
        self.fraction = fraction
        # ReportFixtureの戻り値で振る舞いが変わる
        # [2] b2RayCastCallback Class Referenceを参照
        return fraction
lidar = [LidarCallback() for _ in range(ray_n)]
  1. Fixtures - Box2D tutorials - iforce2d
  2. Box2D: b2RayCastCallback Class Reference

RayCast

Lidarのレーザー起点と有効圏で初期化してRayCastする。当たり判定に伴う処理はReportFixtureで定義済み。

import numpy as np
# ロボットの定義(再掲)
o = np.array([50., 150.]) # ロボット位置
ray_l = 200 # Lidar有効距離
ray_n = 9   # Lidar線数
ray_angles = [math.pi/2 * i / (ray_n - 1.) - math.pi/4 for i in range(ray_n)]
ray_ends = o + ray_l * np.array([[math.cos(theta), math.sin(theta)] for theta in ray_angles])

for l, p in zip(lidar, ray_ends):
    l.p1 = o
    l.p2 = p
    world.RayCast(l, l.p1, l.p2)

シミュレーション内のオブジェクト取得

C++/Javaのインタフェースでは定義されているworld.getBodies()はPythonでは提供されておらず、world.bodiesでアクセスする。

import matplotlib.pyplot as plt
for b in world.bodies:
    for f in b.fixtures:
        w = f.shape.vertices
        # 隔壁が2点で定義されている前提の描画処理
        plt.plot([w[0][0], w[1][0]], [w[0][1], w[1][1]], color = 'black')
for l in lidar_b2d:
    plt.plot([l.p1[0], l.p2[0]], [l.p1[1], l.p2[1]], color = 'blue')
plt.show()

参考

tkinterでOpenAIのgym描画を実装

OpenAI gymを自前で作成する場合、環境の状態を描画するviewerをどうするかかなり迷います。PFRLstable-baselines3で強化学習を進めるならば

env = gym.make('環境名') # または class my_env(gym.Env) のインスタンス作成
for _ in range(100):
    env.step(action)
    env.render()

というコードでtrainer側がループのペースをコントロールする必要があります。こういったフレームワークが整備されていなかった6年前のChainerでやってみるDeep Q Learning - 立ち上げ編 - Qiitaでは、環境の方にtrainerを組み込み、GUIアプリの処理の合間に強化学習をしていました。 現状のプラクティスとしては下記を見かけます。

サーバー/クライアント方式
ROSを介する環境などで良く見かける。Viewerをサーバにして、クライアントであるgym環境から描画に必要な情報を書き込む。Unityの環境もこの形式なのかな?未チェックだけど。
matplotlib方式
matplotlibのアニメーション プロットを使う。renderからmode='rgb_array'で実装されるような内容を受け取るか、envの内部情報で、しこしこお絵描きする。
マルチプロセス方式
GUI側のクライアントを別プロセスで立ち上げ、env側にハンドルをもたせ描画のタイミングを操作する。envを終了するときにGUIのプロセスをきっちり終了させる必要がある。
pygame方式
詳細はよく知らない。OpenAIのbox2d環境で使われているっぽい。

いずれも一長一短なのですがpygame方式よりも低レベルなモジュールで実装したいと思ったので、tkinterで実装する場合を開拓しました。デメリットはstep()の処理が重いと色々おしゃかになること。

class RenderWindow(tk.Tk):
    def __init__(self, env) -> None:
        super().__init__()
        self.render_offset = 5
        self.geometry("%dx%d"%(
                env.width  + self.render_offset * 2,
                env.height + self.render_offset * 2
            )
        )
        self.canvas = tk.Canvas(self,
            width = env.width + self.render_offset * 2,
            height = env.height + self.render_offset * 2,
            bg="#fff"
        )
        self.canvas.place(x = 1, y = 1)

    def render(self, objects):
        h = self.winfo_height()
        w = self.winfo_width()
        self.canvas.delete('all')
        self.canvas.create_rectangle(0, 0, w, h, fill = 'white')

        # オブジェクト郡の描画処理
        for obj in objects:
          if obj.property == RECT:
            # 矩形の描画
          elif ...

class FlatWorld(gym.Env):
    def __init__(self) -> None:
        super().__init__()

        self.width = 640
        self.height = 480

        # 中略

        self.window = None
        self.reset()

    def render(self, mode = 'human', close = False):
        if self.window is None:
            self.window = RenderWindow(self)
        self.window.render( self.objects )
        # self.window.mainloop()と違って実行をブロックしない
        self.window.update_idletasks()
        self.window.update()

おまけ

wxPythonでも行けそうだぜ!pyQtはダメ。おとなしくマルチプロセス/マルチスレッドにするのが良い。

参考

2D Lidarのシミュレーション - 線分交差判定

かなり前にQiitaで2D世界で模擬Lidarを持つロボットの強化学習に取り組みました。元ネタはStanford大学のConvNetJS Deep Q Learning Reinforcement Learning with Neural Network demoで、オリジナルはJavascriptで組まれています。

robot

物理エンジンなどは使わず距離計算をしたのですが、そこで使った数学のおさらいをします。

1. 線分の交差

平面世界の外枠やセパレーションを構成する壁までの距離計算です。Lidarの光線長には有効距離があり、壁も有限長なので、それぞれLidarをモデル化した線分と壁をモデル化した線分の交差判定を使います。

wall

線分が交差する場合の交差点

Lidarの光線1本分と壁を取り出して点O, A, B, Cを下図のように定めます。

wall

さらに仮に線分が「交差する」とし、交点をPとすれば、点Pが存在することが線分が交差する条件となり、また2点間の距離OPが求めたい「距離」となります。点Pは、$ \vec{OA} $ と $ \vec{OB} $ の内分点であって、$ \vec{OP} $ は $ \vec{OC} $ のスカラー倍です。

wall

そこで変数 $ s $ と $ t $ を導入して方程式をたてると変数に制約付きの線形方程式になります。

\[ \begin{array}{ll} & s \cdot \vec{OA} + (1-s) \cdot \vec{OB} = t \cdot \vec{OC} \\ {\rm s.t.} & 0 < s < 1 \\ & 0 < t < 1 \\ \end{array} \]

線形方程式のところに注目して $ \vec{OA} = \begin{bmatrix} a_x & a_y \end{bmatrix} ^{\intercal} $, $ \vec{OB} = \begin{bmatrix} b_x, b_y \end{bmatrix} ^{\intercal} $,$ \vec{OC} = \begin{bmatrix} c_x, c_y \end{bmatrix} ^{\intercal} $ と置くと、

\[ \begin{bmatrix} a_x - b_x & - c_x \\ a_y - b_y & - c_y \end{bmatrix} \begin{bmatrix} s \\ t \end{bmatrix} = - \begin{bmatrix} b_x \\ b_y \end{bmatrix} \\ \]

線分が交差する(⇒ 平行でない)ならば、$ \text{det} \begin{bmatrix} a_x - b_x & - c_x \\ a_y - b_y & - c_y \end{bmatrix} \neq 0 $ なので、逆行列をかけて(線形方程式を解いて)、

\[ \begin{bmatrix} s \\ t \end{bmatrix} = \frac{1}{(a_x - b_x) c_y - (a_y - b_y) c_x } \begin{bmatrix} - c_y & c_x \\ - a_y + b_y & a_x - b_x \end{bmatrix} \begin{bmatrix} b_x \\ b_y \end{bmatrix} \\ \]

さらに、$ 0 < s < 1 $ かつ $ 0 < t < 1 $ を満たすとき線分が交差し、点Pは $ \vec{OP} = t \vec{OC} $ で表されます。

交差しない場合とコーナーケース

さて交差する前提で方程式を立てて解を求めましたが、解なしとなる場合や交差状態として特殊な場合について考察します。いずれも線分としては交差しません。

解が求まらないケース

$ \text{det} \begin{bmatrix} a_x - b_x & - c_x \\ a_y - b_y & - c_y \end{bmatrix} = 0 $ のケースでは先の方程式を解けません。このケースでは線分 $ \text{AB} $ と 線分 $\text{OC} $ とが平行ないしは重なっています。

拘束条件の境界あるいは外の場合

$ 0 < s < 1 $ かつ $ 0 < t < 1 $ でない場合、つまり、$ s ≥ 0 $ または $ 1 ≥ s $ または $ t ≥ 0 $ または $ 1 ≥ t $ の場合です。これらの場合は、点Pは、$ \vec{OA} $ と $ \vec{OB} $ の外分点、または$ \vec{OP} $ が $ \vec{OC} $ より長かったり、逆向きだったりします。 境界では、点Pが点O, A, B, Cのいずれかと一致しますが、これは今回は交差なしとして除外してしまいます。

Pythonコード

以上をまとめてPythonコードにします。 $ o = (o[0], o[1]), a = (a[0], a[1]) \dots $ の要領で配列を定義して上記計算をコードに落とします。

def get_intersection(o, a, b, c):
    """
    線分の交点を求める

    Parameters
    ----------
    o : float[2]
        点Oの平面座標
    a : float[2]
        点Aの平面座標
    b : float[2]
        点Bの平面座標
    c : float[2]
        点Cの平面座標

    Returns
    -------
    bool
        線分に交点があるか
    float[2]
        点Pの平面座標。交点がない場合はNone.
  """
    ob = [b[0] -  o[0], b[1] - o[1]]
    oc = [c[0] -  o[0], c[1] - o[1]]
    # oa - ob = ba
    ba = [a[0] -  b[0], a[1] - b[1]]

    det = ba[0] * -oc[1] - ba[1] * -oc[0]

    # 両線分が平行のケース
    if det == 0.0:
        return False, None

    s = - (-oc[1] * ob[0] + oc[0] * ob[1]) / det
    t = - (-ba[1] * ob[0] + ba[0] * ob[1]) / det

    if 0 < s and s < 1.0 and 0 < t and t < 1.0:
        return True, [t*oc[0] + o[0], t*oc[1] + o[1]]

    # 線分に交点がないケース
    return False, None

デモ

上記と同じものをJavascriptに移し替えてCanvasに描いたものがこちらです。隔壁(縦線)が光線を遮っています。

2. 円と線分の交差

報酬のトリガーとなる得点源は円で表現しています。Lidarがこれらまでの距離を測れるように円と線分の交差点を求めます。

robot

円と線分が交差する場合

基本的には線分同士と同じように「交差する」前提で方程式を立てて、解がないケースを検証します。やっかいなケースのほうが共感を集めやすいと思い、光線終点が円の外にあり、交差点が2つあるケースを図にしました。 今回の興味は光線の始点(点O)に近い点Pまでの距離です。 なかなか吸引力のある問題のようでStackoverflowがもりあがっていました。

ball

とはいえ単純に $ \vec{AO} $ と $ \vec{AC} $ の内分点 $ P $ で $ \Vert AP \Vert = r $ となる点を求めるとしてよいですね (ただしrは円の半径)。

\[ \begin{array}{ll} & \Vert s \cdot \vec{AO} + (1-s) \cdot \vec{AC} \Vert = r \\ {\rm s.t.} & 0 < s_0 < 1\\ & s_0 \geq s_1 \\ \end{array} \]

距離の方程式だけに注目して $ \vec{AO} = \begin{bmatrix} o_x & o_y \end{bmatrix} ^{\intercal} $, $ \vec{AC} = \begin{bmatrix} c_x, c_y \end{bmatrix} ^{\intercal} $ と置くと、

\[ \begin{array}{crl} & s^2 (o_x^2 + o_y^2) + 2 s(1-s) (o_x c_x + o_y c_y) + (1-2s+s^2) (c_x^2 + c_y^2) &= r^2 \\ \Leftrightarrow & ((o_x - c_x)^2 + (o_y - c_y)^2) s^2 + 2 (o_x c_x + o_y c_y - c_x^2 - c_y^2) s + (c_x^2 + c_y^2 - r^2) & = 0 \end{array} \]

これは二次方程式なので二次方程式の解の公式 - Wikipediaでも参照してください。

交差しない場合とコーナーケース

解がないケース

「判別式が負になった」が典型的なケースで、これは線分と円が交差しません。また、点Cが円の内部にある場合は $ 0 > s_1 $ となり $ s_0 $ のみが交点です。

拘束条件境界

線分のときと同じ扱いにしました。

Pythonコード

まとめてPythonコードにします。 $ o = (o[0], o[1]), a = (a[0], a[1]) \dots $ の要領で配列を定義して上記計算をコードに落とします。

def get_intersection(o, c, a, r):
    """
    線分の交点を求める

    Parameters
    ----------
    o : float[2]
        点Oの平面座標
    c : float[2]
        点Cの平面座標
    a : float[2]
        円の中心の平面座標
    r : float
        円の半径

    Returns
    -------
    bool
        線分と円に交点があるか
    float[2]
        点Pの平面座標。2つ交点があるときは点Oに近い方。交点がない場合はNone.
  """
    ao = [o[0] - a[0], o[1] - a[1]]
    ac = [c[0] - a[0], c[1] - a[1]]
    co = [o[0] - c[0], o[1] - c[1]]

    eq_a = co[0] ** 2 + co[1]**2
    eq_b = ao[0] * ac[0] + ao[1]*ac[1] - ac[0] ** 2 - ac[1] ** 2
    eq_c = ac[0] **2 + ac[1] ** 2 - r **2

    discriminant = eq_b * eq_b - eq_a * eq_c
    # 解なしのケース
    if discriminant < 0:
        return False, None

    discriminant = discriminant ** 0.5

    s0 = (- eq_b + discriminant)/eq_a
    s1 = (- eq_b - discriminant)/eq_a

    if 0 < s0 and s0 < 1.0:
        return True, [s0 * x + (1 - s0)* y + z for (x, y, z) in zip(ao, ac, a)]

    if 0 < s1 and s1 < 1.0:
        return True, [s1 * x + (1 - s1)* y + z for (x, y, z) in zip(ao, ac, a)]

    # 線分に交点がないケース
    return False, None

デモ

同様にJavascriptに移し替えてCanvasに描いたものがこちらです。ちょっと大きめの円。

参考

画像分類カスタムデータの読み込み (Pytorch 1.10)

環境

  • python 3.8
  • CUDA 11.3
  • pytorch 1.10
  • torchvision

カスタムデータの読み込み

データセットがサブディレクトリ名をラベルとして下記のように保存されているとして

dataset
  ├─dog
  │  ├─001.png
  │  ├─002.png
  │  ...
  └─cat
     ├─001.png
     ├─002.png
     ...

ImageFolderを使ってデータを読み込む。OnlineなAugumentationをローダーに付与することもできる。

import torchvision

dataset = torchvision.datasets.ImageFolder(
  './dataset',
  torchvision.transform.Compose([
    torchvision.transform.RandomHorizontalFlip(),
    torchvision.transform.ToTensor(),
    torchvision.transform.Normalize(mean = [0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
  ])
)

このときラベルはdatset.class_to_idxでdict型で{'label': 0, ...}として取得できるのでclasses = list(dataset.class_to_idx.keys())でidからラベルを得るリストを作れる。

データローダーへのセット

読み込んだデータはそのままDataLoaderへ…

import torch
data_train = torch.utils.data.DataLoader(dataset, batchsize = 64, shuffle = True)

読み込んだ画像がRGBのカラー画像だとして表示するときは

import matplotlib.pyplot as plt
sample_image, label = iter(data_train)
plt.imshow(sample_image[0].permute(1, 2, 0))

参考

Python-pptxでヒートマップを作る

Python-pptxでパワーポイントにヒートマップを作る。ベースとして表を使う。

from datetime import datetime
import colorsys

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor

import numpy as np

file_name = "{}.pptx".format(datetime.now().strftime("%Y-%m-%d-%H%M%S"))

presentation = Presentation("./template.pptx")
layout = presentation.slide_layouts[0]
for shape in layout.placeholders:
	print('%d %s' % (shape.placeholder_format.idx, shape.name))

slide = presentation.slides.add_slide(layout)
slide.placeholders[0].text = "Dash Board"

heatmap_data = np.random.rand(5, 20).flatten()
# PlaceholderにTableを挿入
table = slide.placeholders[13].insert_table(rows = 5, cols = 20).table

for i, cell in enumerate(table.iter_cells()):
		cell.text_frame.paragraphs[0].font.size = Pt(5)
		cell.fill.solid()
		cellCol = colorsys.hsv_to_rgb(0.7, heatmap_data[i], 1.0)
		cell.fill.fore_color.rgb = RGBColor(*[int(x*255) for x in cellCol])
		cell.height = Pt(5)

for row in table.rows:
		row.height = Inches(0.25)

for col in table.columns:
		col.width = Inches(0.25)

presentation.save(file_name)

WindowsのイベントをPythonで取得

WindowsのイベントをPythonで取得する方法のメモ

試しに起動とシャットダウンの時刻を取得し表示した

import win32evtlog
import winerror

h_event_log = win32evtlog.OpenEventLog("localhost", "System")
flags = win32evtlog.EVENTLOG_FORWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
events = win32evtlog.ReadEventLog(h_event_log, flags, 0)

# [Windowsの起動やスリープなどの履歴を調べてみた! – A2\-blog](https://edit-anything.com/blog/windows-power-log.html)
target_ids = [6005, 6006, 6008, 7001, 7002]
start_end_map = {
  6005: True,
  6006: False,
  6008: False,
  7001: True,
  7002: False
}
events = True
while events:
  events = win32evtlog.ReadEventLog(h_event_log, flags, 0)
  for event_obj in events:
    event_time = event_obj.TimeGenerated
    event_id = winerror.HRESULT_CODE(event_obj.EventID)

    if event_id in target_ids:
      if start_end_map[event_id]:
        print(event_id, event_time)
      else:
        print("\t", event_id, event_time)

連続時間システムの離散化手法の比較 [Python Scipy]

MPC制御などでは、システムを離散時間で表した状態空間モデルを使います.その一方で立式の段階では微分方程式を立てるので、 連続時間のシステムを離散化する必要があります.

連続時間 離散時間
\(\dot{x} = A x + B u\) \(x_{k+1} = A x_{k} + B u_{k} \)

システムの離散化にはPythonのモジュールのScipyにscipy.signal.cont2discreteという大変便利なメソッドがありますが、v1.63の時点で離散化方法が下記のように7つもあります. 流石に多すぎてどう違うのかわからないので、ちょっと試してみましょう. gbtだけは他の手法の一般化という位置づけなので、比較の対象は6種類です.

  1. gbt: generalized bilinear transformation
  2. bilinear: Tustin’s approximation (“gbt” with alpha=0.5)
  3. euler: Euler (or forward differencing) method (“gbt” with alpha=0)
  4. backward_diff: Backwards differencing (“gbt” with alpha=1.0)
  5. zoh: zero-order hold (default)
  6. foh: first-order hold (versionadded: 1.3.0)
  7. impulse: equivalent impulse response (versionadded: 1.3.0)

最初に結果だけ見たい

はい、まず実験台となるシステム、ドン.

\[ G(s) = \frac{10}{s^2 + 3s + 10} \]

システムの出典: Convert model from continuous to discrete time - MATLAB c2d - MathWorksより、むだ時間を削除

伝達関数で表現された2次遅れ系ですね.こちらの伝達関数を状態空間の形にします.

\[ \begin{array}{rl} \dot{x} &= \begin{bmatrix} 0 & 1 \\ -10 & -3 \end{bmatrix} x + \begin{bmatrix} 0 \\ 10 \end{bmatrix} u \\ y &= \begin{bmatrix} 1 & 0 \end{bmatrix} x \end{array} \]
従いまして、Scipyのシステム定義 \(A, B, C, D\)とする行列はこちらです.
\[ \begin{array}{rl} A &= \begin{bmatrix} 0 & 1 \\ -10 & -3 \end{bmatrix} \\ B &= \begin{bmatrix} 0 & 10 \end{bmatrix}^{\intercal} \\ C &= \begin{bmatrix} 1 & 0 \end{bmatrix} \\ D &= \begin{bmatrix} 0 \end{bmatrix} \end{array} \]

離散化を dt = 0.1 [sec] として比較します.

import numpy as np
from scipy.signal import cont2discrete, lti, dlti, dstep
import matplotlib.pyplot as plt

# システム係数
A = np.array([[0, 1],[-10, -3]])
B = np.array([[0],[10]])
C = np.array([[1, 0]])
D = np.array([[0]])

# 離散時間のステップ値[sec]
dt = 0.1

# 連続時間システムの作成
l_system = lti(A, B, C, D)
## 正解データ向けステップ応答取得
t, x = l_system.step(T=np.linspace(0,5,100))

# 制御入力のプロット(ステップ応答)
plt.hlines(1, t[0], t[-1], linestyles='dotted')
# 正解データのプロット
plt.plot(t, x, label='Ground Truth')

# 6つの方法を試す.デフォルトのZOHが最初.
for method in ['zoh', 'bilinear', 'euler', 'backward_diff', 'foh', 'impulse']:
    # システムの離散化
    d_system = cont2discrete((A, B, C, D), dt, method=method)
    ## 離散データのステップ応答取得
    s, x_d = dstep(d_system)
    x_d = np.squeeze(x_d)
    plt.step(s, x_d, label=method, where='post')

plt.xlim(t[0], t[-1])
plt.ylim(x[0], 1.4)

plt.ylabel('Amplitude')
plt.xlabel('Time [s]')
plt.title('Step Response')
plt.legend()
plt.savefig('000.svg')

結果は下記です.Eulerさん2名が残念な以外は、ちょっと多すぎて何言ってるかわからないですね.

Root Mean Square Errorをとってみると順位はこちら.

手法 RMSE
zoh 3.80e-15
impulse 1.92e-01
bilinear 1.92e-01
foh 2.04e-01
backward_diff 3.74e-01
euler 4.50e-01

上位3手法を取り出すとこうなってます.ZOHが優秀です.

離散化の理論

連続時間のシステムモデル

\[ \dot{x} = A x + B u \\ \]
に対しては、Wikipedia JP 離散化で導出が詳しく書かれている通り、
\[ x_{k+1} = e^{A \Delta \text{t}} x_k + A^{-1} (e^{A \Delta \text{t}} - I) B u_k \\ \]
という厳密解が求まる.これにより
\[ \begin{array}{rl} A_d &= e^{A \Delta \text{t}} \\ B_d &= A^{-1} (e^{A \Delta \text{t}} - I) B \\ C_d &= C \\ D_d &= D \\ \end{array} \]

Euler

理論に対して非線形システムの局所線形化など線形時不変システム以外の条件だと \(e^{A \Delta \text{t}}\)の計算ちょっと大変すぎるよね. \(A^{-1}\)だってしょっちゅう計算したくないし.

というわけで、

\[ e^{A \Delta \text{t}} \approx I + A \Delta \text{t} \]
としてしまって
\[ \begin{array}{rl} A_d &= I + A \Delta \text{t} \\ B_d &= B \Delta \text{t} \\ C_d &= C \\ D_d &= D \\ \end{array} \]
と簡略したのがこちらの手法.

Backward

同様に\(e^{A \Delta \text{t}} \approx \left(I - A \Delta \text{t} \right)^{-1} \) と近似したもの.式を整理して、こちら.

\[ \begin{array}{rl} A_d &= (I - A \Delta \text{t})^{-1} \\ B_d &= (I - A \Delta \text{t})^{-1} B \Delta \text{t} \\ C_d &= C (I - A \Delta \text{t})^{-1} \\ D_d &= D + C \cdot B_d \\ \end{array} \]
離散化した状態方程式・観測方程式は添字が少しずれるので注意.
\[ \begin{array}{rl} x_{k} &= A_d \color{red}{x_{k-1}} + B_d u_{k} \\ y_{k} &= C_d \color{red}{x_{k-1}} + D_d u_{k} \\ &= C (A_d x_{k-1} + B_d u_{k}) + D u_{k} \\ &= C x_{k} + D u_{k} \\ \end{array} \]

Bilinear / Tustin

同様に

\[ \begin{array}{rl} e^{A \Delta \text{t}} &= \frac{e^{A \Delta \text{t}/2}}{e^{-A \Delta \text{t}/2}} \\ & \approx \left(I + \frac{1}{2} A \Delta \text{t} \right) \left(I - \frac{1}{2} A \Delta \text{t} \right)^{-1} \end{array} \]
と近似したもの

Impulse

前記の厳密解を導出する過程では

\[ x_{k+1} = e^{A \Delta \text{t}} x_k + \int_0^{\Delta \text{t}} e^{Av} B u_k \text{d} v \\ \]
を計算している.この際、\([0, \Delta \text{t} ]\) で、\(u_k\) 一定としているが、 ここを一定ではなくインパルス \( \delta(v) u_k \) とする. すると \(B_d \) の計算結果が変わり、
\[ \begin{array}{rl} A_d &= e^{A \Delta \text{t}} \\ B_d &= e^{A \Delta \text{t}} \Delta \text{t} B \\ C_d &= C \\ D_d &= D \\ \end{array} \]

ZOH: Zero Order Hold

デジタル制御:最初の最初 ①で触れたZOHそのもの.制御入力uは、通常、制御周期間では定数(ZOH)であって \( \dot{u} = 0 \) といえる. この条件を使ってシステム方程式を書き直すと

\[ \begin{bmatrix} \dot{x} \\ \dot{u} \end{bmatrix} = \begin{bmatrix} A & B \\ 0 & 0 \end{bmatrix} \begin{bmatrix} x \\ u \end{bmatrix} \]

改めてベクトルrと行列Mを下記のように定義すると

\[ \begin{array}{rl} r &= \begin{bmatrix} x & u \end{bmatrix}^{\intercal}\\ M &= \begin{bmatrix} A & B \\ 0 & 0 \end{bmatrix} \end{array} \]
上記方程式は、\( \dot{r} = M r \) であって、この方程式の解は、\( r(t) = e^{Mt} r(0) \) . これより、\(t_{k+1} = t_{k} + \Delta \text{t} \) と置くと、 \(x_{k+1} = x(t_{k+1}) \) であるので
\[ \begin{bmatrix} x_{k+1} \\ u_{k+1} \end{bmatrix} = e^{M \Delta \text{t}} \begin{bmatrix} x_k \\ u_k \end{bmatrix} \]
以上を開いて \(M_d = e^{M \Delta \text{t}} \) を計算してやると
\[ M_d = \begin{bmatrix} A_d & B_d \\ 0 & I \end{bmatrix} \]
となるので、これをありがたく使う. このようにステップ応答でZOHが精度無双したのは、導出の前提(制御入力ZOH)とステップ応答が同じ解を求めていることによる.

伝達関数の離散化についてはZ変換表も使えるので、プラントモデルの離散化:Z変換表も是非どうぞ.

FOH: First Order Hold

(レビュー必要 - 誤解あったので修正予定)

ZOHと似たようなことをする. まずZOHで求めた解を眺めると

\[ \begin{bmatrix} x_{k+1} \\ u_{k+1} \end{bmatrix} = \begin{bmatrix} A_d & B_d \\ 0 & I \end{bmatrix} \begin{bmatrix} x_k \\ u_k \end{bmatrix} \]
\(u_{k+1}=u_{k}\) となっていることがわかる. DDP(Differential Dynamic Programming)などの制御だと低次制御(比例制御)と高次制御(MPC制御)のカスケードにすることがあり、 \(u_{k+1}=u_{k}\) とは言えなくなるほど誤差が大きい場合がある. そこで、\( \dot{u} = 0 \) をちょっと見直す.

もし、 \(k\) の時点で \(u_{k+1}\) の値がわかっているなら、 \(\dot{u} = \frac{u_{k+1} - u_k}{\Delta \text{t}} \) と置いた方が精度が高まる.これがFOHの考え方. ところが実際にはわからない事が多いので、scipyの実装では固定的に \(\dot{u} = \mathbb{1}/ \Delta \text{t} \) と置いている. これは投機的な予測になるので当たれば精度が高まるが、その逆ではズレが大きくなると思う. また、\(u_k\) が充分に大きいと \(u_{k+1} \approx u_k + 1 \) となり、ZOHとあまり変わらないのではという予感も. 兎にも角にも、Scipyの実装にならえば、

\[ \begin{array}{rl} \begin{bmatrix} \dot{x} \\ \dot{u} \\ 0_n \end{bmatrix} &= \begin{bmatrix} A & B & 0 \\ 0 & 0 & \mathbb{1}_{n \times n}/ \Delta \text{t} \\ 0 & 0 & 0 \end{bmatrix} \begin{bmatrix} x \\ u \\ \mathbb{1}_n \end{bmatrix} \\ s.t. & u \in \mathcal{R}^{n} \end{array} \]
と方程式をたてて、あとはZOHと同じフローで \(A_d, B_d \) を得る.

参考