Ubuntu 20.04 LTS on WSL2 環境構築メモ (3/4) - Docker 導入

前提

導入対象とするマシンは下記の通り。

  • Hardware : CPU = i10900, GPU = GeForce RTX 2080Ti, Mem = 32GB, SSD 1 TB + HDD 2TB
  • ベース OS : Windows 10 Pro バージョン 20H2 (ビルド 19042.685)
  • 仮想 OS : Ubuntu 20.04 LTS on WSL2
  • 環境構築メモ 2/4 を実施した状況

Docker 導入

Ubuntu 14.04 より apt-get, apt-chache ではなく apt コマンドの使用が推奨されているため、参考記事に旧コマンドが含まれている場合は注意。特に Docker Package の導入では apt 新コマンド利用を強く推奨されている。

Docker のインストールと設定

前提パッケージのインストール
 sudo apt update
 sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
Docker 公式 GPG 公開鍵のインストール
 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
apt レポジトリの設定

Intel x86-64 だけど arch=amd64 でいいの?」と思うが、x86-64 (= AMD64) とは AMD が開発した 32bit 下方互換 64bit アーキテクチャという意味なのでこれでよい。AMD ではない方と誤って armhf にしてはいけない。

 sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Docker Community Edition のインストール
 sudo apt update
 sudo apt install -y docker-ce

# 最後に更新
sudo apt update; sudo apt upgrade
インストールした Docker のバージョン確認
 docker version
一般ユーザで Docker を起動するための準備

インストールした状態では root しか docker コマンドを実行できないため、起動させたいユーザを docker グループに追加する。現状のユーザをグループ追加するには次のコマンドを実行する。

 sudo usermod -aG docker ${USER}

設定は再ログインまたは次のコマンドを実行した後に有効になる。

 su - ${USER}

次のコマンドを実行し、指定したユーザが docker グループに所属したか確認する。

 id -nG

これで Docker のインストールが完了し、デーモンが起動している、確認するため次のコマンドを実行する、と各解説サイトは説明する。

 sudo systemctl status docker

が、実行結果は下記の通り。デーモンプロセスは動いていない。

System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

systemd ≠ pid 1 問題とその解決

ここでデーモンが動かないのは、一般の Unix システムでカーネルから最初に起動され pid = 1 を割り振られる init プロセスが、仮想 OS である Ubuntu on WSL2 においてはカスタマイズされてしまっていることによる。Unix の場合 init = systemd (pid 1) だが、WSL2 の場合、init プロセスは systemd とは別にあり、systemd はインストールされているものの無効化されていて、有効化しても pid = 1 にはならない。という背景により systemctl によるサービス管理が動作しない。

解決方法はいくつかあるらしいが、ここでは Genie というものを導入する方法を採る。

Genie 導入の準備

Genie が依存するパッケージをあらかじめインストールする。

 sudo apt install -y daemonize dbus policykit-1

.NET Runtime が必要となるため、最新の .NET 5.0 for Ubuntu 20.04 LTS をインストールする。(参考記事は .NETLinux も古いため、手順は少しカスタマイズが必要。)

# レポジトリの追加
 wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb

# 更新
 sudo dpkg -i packages-microsoft-prod.deb
 sudo apt update

# .NET Runtime 導入
 sudo apt install -y apt-transport-https && sudo apt update && sudo apt install -y aspnetcore-runtime-5.0

Genie の導入

Genie のインストール

まず、下記の内容を記述した /etc/apt/sources.list.d/wsl-translinux.list というファイルを新規作成する。書き込みには root 権限が必要。本家で説明されている導入手順であるため、解説サイトなどでは説明が端折られていることが多いが、このファイルを作成しないとパッケージが見つからず、インストールが途中で止まる。

 deb [trusted=yes] https://wsl-translinux.arkane-systems.net/apt/ /

Genie をインストールする。

# リポジトリの追加
 curl -s https://packagecloud.io/install/repositories/arkane-systems/wsl-translinux/script.deb.sh | sudo bash

# Genie のインストール
 sudo apt install systemd-genie
getty@tty サービスの停止と systemd への差し替え
 sudo systemd stop getty@tty
Genie の起動確認
# Genie の動作確認 (エラーが出なければ OK)
 genie -s
systemctl による Docker の状況再確認

あらためて systemctl で Docker の状態を再確認する。.NET 入れたり Genie 入れたり、これらも一筋縄ではいかず、苦労したが、やりたかったのはこれ。ふぅ。

 sudo systemctl status docker
Genie の自動起動設定

WSL2 起動時に Genie を自動起動させるために /etc/profile.d/genie.sh に管理者権限で下記の設定をする。(参考記事と少し異なる理由は後述)

# systemd の PID を調べるコマンドにエイリアスをつける
alias getsdpid='ps -eo pid,lstart,cmd | grep systemd | grep -v -e grep -e systemd- | sort -n -k2 | awk '\''NR==1 { print $1 }'\'''

if [ -z "`getsdpid`" ]; then
  echo 'genie : initializing ...'
  genie -i &> /dev/null &
  echo 'genie : waiting for initialization ...'
  sleep 2s  
fi

if [ "`getsdpid`" != "1" ]; then
  echo 'genie : launching bash ...'
  genie -s
fi
Ubuntu 仮想環境初回起動時に systemd を待ち続ける問題

WSL2 起動時に Genie を自動起動させるために .bashrc/etc/profile.d/genie.sh に下記を追記しておけとよく解説されているが、これでは PC 起動直後の Ubuntu 仮想環境初回起動時には Waiting for systemd ... !!!!!!!! (! が次第に増えていく) と出続け、しばらく待っていても機能しない。

if [ "`ps -eo pid,lstart,cmd | grep systemd | grep -v -e grep -e systemd- | sort -n -k2 | awk 'NR==1 { print $1 }'`" != "1" ]; then
  genie -s
fi

この問題にかなり悩まされたがいろいろと調査した結果、わかったことを列挙する。原因は初期化プロセスでかなり待たされるようになっている Genie の作りにあるようだ。

  • genie -s は、①初期化 (systemd 関連プロセス群の起動) を担う genie -i の部分と、②bash シェルを (Ubuntu 仮想環境直上の bash に重ねて) 起動する部分、から成る
  • genie -s は、初期化されていれば①をスキップする
  • genie -i は、systemd 関連プロセス起動をキックした後に Waiting for systemd ... !!!!!!!! と出力してコンソールを占有しつつ、数分間稼働し続ける
  • systemd 関連プロセス群が利用可能になるまでの時間は数秒だが、②の bash シェル起動は①のプロセス完了を待っている
  • /init から systemd への pid = 1 移し替えは genie -s が担っている (genie -i ではない)
  • systemd を pid = 1 にしているのはエミュレーションであり、genie -s が起動した bash シェル上でのみ有効である (Ubuntu 仮想環境直上で実際に systemd が pid = 1 となっているわけではない)
  • genie -i が起動した systemd 関連プロセス群は genie -u でクリーンアウトできる


したがって、回避策としては下記のようになる。

  • genie -s の担う役割を、①初期化、と、②bash シェル起動 + pid エミュレート、とに分離する (初期化を genie -s に頼らない)
  • systemd がなければ genie -i を起動して初期化する
  • genie -i は、出力を捨てつつバックグラウンドで立ち上げる
  • 初期化開始後に genie -s を起動して bash シェルを起動する (genie -i の完了を待たない)


genie.shの冒頭に記述した systemd の pid を返すエイリアス getsdpid を使うと下記のように環境を調べることができる。

  • 1 が返る → genie -s が起動した bash にいる
  • 1 以外が返る → genie -i が起動した systemd は生きているが、Ubuntu 仮想環境直上の bash にいて systemd の pid は 1 ではない
  • 何も返らない → Ubuntu 仮想環境直上の bash にいて systemd も生きていない


.bashrc もレイヤー違いで重ねて走ってしまうため、重ねると害のある部分 (hwclock など) は getsdpid の返り値によってスキップさせるとよい。

Docker Compose のインストール

Docker Compose も使うことになるため、インストールしておく。

# update package list
sudo apt update
sudo apt upgrade

# install
sudo apt install docker-compose

Docker 利用の省力化

テキストタイプ省力化のためによく使う docker コマンドのエイリアス.bashrc に記述しておく。

alias dc='docker'
alias dcc='docker-compose'
alias dce='docker exec -it $(docker ps -lq) /bin/bash'

Ubuntu 20.04 LTS on WSL2 環境構築メモ (2/4) - GUI 導入

更新 2022.10.09

導入保留としていた WSLg preview を Widows 11 が勝手にアップグレード導入し、VcXsrv と競合するようになってしまったため、泣く泣く VcXsrv を外すことにする。
具体的には XLaunch を起動しないようにし、~/.bash_profile の export DISPLAY 設定を外すだけ。
タスクトレイの XLaunch に頼らなくなったことと LinuxWindows 間でコピペができるようになったのは WSLg の長所だが、初回起動が遅い (2 回目以降は速い)、ウィンドウのデザインや操作感がちょっとダサい、PowerToys の FancyZone 対象外になってしまうのが短所。


前提

導入対象とするマシンは下記の通り。

  • Hardware : CPU = i10900, GPU = GeForce RTX 2080Ti, Mem = 32GB, SSD 1 TB + HDD 2TB
  • ベース OS : Windows 10 Pro バージョン 20H2 (ビルド 19042.685)
  • 仮想 OS : Ubuntu 20.04 LTS on WSL2
  • 環境構築メモ 1/4 を実施した状況

GUI 導入

Ubuntu 14.04 より apt-get, apt-chache ではなく apt コマンドの使用が推奨されているため、参考記事に旧コマンドが含まれている場合は注意。

X-Window Server (VcXsrv) のインストールと設定

VcXsrv インストール

VcXsrv のインストールは参考記事 初心者のためのWSL( 2 ) ~GUI設定,デスクトップ環境設定編~ のとおり。記事を参考にするのはインストール作業まで。VcXsrv 初期設定以降は参考にするだけで実行しない。

VcXsrv の初期設定と起動確認

XLaunch (VcXsrv のアイコン) をクリックし、初期設定と起動確認を行う。

既定のとおり。Multiple windows を選択 *1

既定のとおり。

Disable access control にチェックを入れる。

Save configuration を押し、config.xlaunchshell:startup へ保存する。その後、完了を押す。

タスクトレイに XLaunch のアイコンが表示されれば起動成功。都度、XLaunch 起動・初期設定をするのは面倒であるため、上記の最終ステップでスタートアップメニューに設定を保存している。

UbuntuGUI 設定
# パッケージのインストール
 sudo apt install x11-apps x11-utils x11-xserver-utils dbus-x11

# bash 起動時の設定とその反映
 echo 'export DISPLAY=xx.xx.xx.xx:0.0' >> ~/.bash_profile
 source ~/.bash_profile

上記 3 行目の設定値 xx.xx.xx.xx は X-Windows Server サービスが走っているホスト OS (Windows 10) 側の IP アドレス。その指定方法は2通りある。

X-Window Server 用の通信設定(動的版)

1つはホスト OS の物理イーサネットアダプタの IP アドレスを固定値指定する方法。もう1つは WSL 起動時に動的に決まるイーサネットアダプタ vEthernet (WSL) の IP アドレスを指定する方法。下記のように指定する。この方法は地のゲスト OS
からだけではなく、ゲスト OS 上の Docker コンテナからの接続でも有効であった。

 export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0.0
Windows Defender 受信規則設定

ただ、この方法を採るにはある事前準備が必要で、この点をきちんと解説しているサイトは非常に少ない。

WSL2 とホスト OS との通信はパブリック扱いとなるらしく、WSL2 ⇔ ホスト OS 間で行う通信が Windows Defender に塞がれないようセキュリティについて詳細設定する必要がある。既定設定だと Windows Defender に VcXsrv サービスのすべておよび WSL2 から イーサネットアダプタ vEthernet (WSL) への ping echo back が塞がれてしまうため、パブリックプロファイルで許可を与える。 その手順は以下の通り。

  1. ping echo back を許可するルールの作成方法
    1. "Windows Defender ファイアーウォール" を開く
    2. "詳細設定" を押す
    3. "受信の規則" を右クリックし、"新しい規則" を選択する
    4. "規則の種類" で "カスタム" を選択し、"次へ" を押す
    5. "すべてのプログラム" を選択し、"次へ" を押す
    6. プロトコルの種類で "ICMPv4" を選択し、"カスタマイズ" を押す
    7. "特定の種類の ICMP" を選択し、"エコー要求" をチェックして "OK" をクリックし、"次へ" を押す
    8. ローカル IP アドレス (ホスト OS イーサネットアダプタ vEthernet (WSL) 側) で "これらの IP アドレス" を選択し、"追加" を押し、"この IP アドレスまたはサブネット" で "172.0.0.0/8" と入力する *2
    9. リモート IP アドレス (ゲスト OS 側) で "これらの IP アドレス" を選択し、"追加" を押し、"この IP アドレスまたはサブネット" で "172.0.0.0/8" と入力し、"次へ" を押す
    10. "接続を許可する" を選択し、"次へ" を押す
    11. "プライベート" と "パブリック" を選択し、"次へ" を押す
    12. 受信規則の名前を付け、"完了" を押す
  2. VxXsrc プロファイルの作成方法
    1. "アプリに Windows Defender ファイアウォール経由の通信を許可する" を押す
    2. "VcXsrv windows xserver" を選択し、"プライベート" および "パブリック" をチェックする
    3. "VcXsrv windows xserver" を選択してチェック(有効化)し、"OK" を押す
    4. 上述の ping echo back の設定方法のとおり、"詳細設定" から XcXsrv サービスの "受信の規則" における "ローカル IP アドレス" と "リモート IP アドレス" を設定する *3*4
    5. "プロパティ" > "詳細設定" > "エッジトラバーサル" は "アプリケーションに従う" を選択しておく
GUI のテスト

次のコマンドを実行し、Windows デスクトップ上に目玉が描画されたら成功。

 xeyes &

デスクトップ環境 (xfce) インストール

Ubuntu デスクトップ環境のインストール手順は以下のとおり。GNOME のインストールをトライしてみたが、参考記事と同様にうまくいかなかったため、軽量の xfce にしてみる。

なお Ubuntu デスクトップ環境はインストールしなくてもよい。VcXsrv が個別アプリごとに Windows 10 デスクトップ上に窓を描画してくれるため、すべての作業を Ubuntu 内で完結させる必要はない。Windows 10 に PowerToys 導入済みの場合は窓の配置を細かく指定できるため、Ubuntu デスクトップ環境に頼らない方がむしろ便利かもしれない。

インストール

次のコマンドを実行する。

 sudo apt install xfce4-terminal
 sudo apt install xfce4-session
 sudo apt install xfce4
 export LIBGL_ALWAYS_INDIRECT=1
起動確認

次のコマンドを実行し、Ubuntu デスクトップ環境が起動するか確認する。なお、Ubuntu デスクトップ環境を使用するときは VcXsrv の起動オプションを "Multiple windows" ではなく "One large window" に設定しておく方が取り回ししやすい。

 startxfce4

*1:Ubuntu デスクトップ環境を利用する場合は Multiple windows ではなく One large window 推奨。

*2:動的に決まるためサブネット指定する。172.17.xx.xx だったり 172.30.xx.xx だったりするため、上位1オクテットのみの指定。

*3:簡易設定のみだと、任意のリモートアクセスが可能になってしまうため、ネットワークのスコープを限定する。

*4:UDP しか使わないのだが、TCP も自動的に作成されて今うため、こちらも同様にスコープを限定しておく。

Ubuntu 20.04 LTS on WSL2 環境構築メモ (1/4) - 開発環境導入

前提

導入対象とするマシンは下記の通り。

  • Hardware : CPU = i10900, GPU = GeForce RTX 2080Ti, Mem = 32GB, SSD 1 TB + HDD 2TB
  • ベース OS : Windows 10 Pro バージョン 20H2 (ビルド 19042.685)
  • 仮想 OS : Ubuntu 20.04 LTS on WSL2

開発環境導入

本ページ手順 Ubuntu 20.04 LTS on WSL2 環境構築メモ (1/4) では問題となるところはないが、以降の手順で参考記事の通りに進まない箇所が多々あるため、環境構築メモとして記録しておく。
Ubuntu 14.04 より apt-get, apt-chache ではなく apt コマンドの使用が推奨されているため、参考記事に旧コマンドが含まれている場合は注意。

日本語フォントのインストール

Ubuntu に日本語フォントをインストールする。後の GUI 設定におけるデスクトップ環境インストール前にやっておくとよいらしい。

 sudo apt install fonts-ipafont

Windows 10 にインストールされているフォントの参照設定

Windows 10 にインストールされているフォントを参照するように設定する。エディタに nano を用いているが何でもよい。

 sudo nano /etc/fonts/local.conf

local.conf に追記する内容は下記のとおり。

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>/mnt/c/Windows/Fonts</dir>
</fontconfig>

開発環境インストール

基礎的な開発ツール群をインストールしておく。

 sudo apt install emacs
 sudo apt install make
 sudo apt install gcc
sudo apt install meld   # including diff etc.

基礎設定

Windows 10 から Ubuntu on WSL2 へのドライブマウント

Windows から WSL2 のドライブへはネットワーク共有フォルダ \\wsl$\ として参照可能であるため、下記のようにドライブレターを割り当てておく

net use U: \\wsl$\Ubuntu-20.04 /persistent:yes

.bashrc と .bash_profile の整備

.bashrc の起動設定

.bashrc を有効にするために .bash_profile に下記の記述を追記しておく。理由は後述のとおり。
Ubuntu on WSL2 が既定で用意している .bashrc が読み込まれるとプロンプトがカラーになるため、カラーか否かをもって .bashrc が有効か否かを判定できる。

# if running bash
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
	. "$HOME/.bashrc"
    fi
fi

bash 起動の際に読み込まれる初期化スクリプトには .profile, .bash_profile, .bashrc の 3 種類がある。が、環境設定を解説する参考記事のとおりに .bash_profile, .bashrc を編集しても .bashrc が読み込まれない。調べてみると、.profile.bashrc を読み込む記述があり、かつ、.profile.bash_profile が存在しているときは読み込まれないらしい。

WSL2 初期状態では .bash_profile がないため、参考記事に従って .bash_profile を新規作成するのみだと .bashrc の起動トリガーであった .profile が無効になってしまい、.bashrc が動作しなくなる *1。したがって、上述のとおり .profile にある .bashrc 起動ロジックを .bash_profile へ移植する。

うっかり対策

Linux を導入すると怖いのが、うっかりフォルダを全削除してしまうこと。Windows / Linux 相互運用などするときには特に注意。いちおう簡易な対策を .bashrc に入れておく。本格的には定期バックアップを仕掛けるべきか。

# rm 実行前確認を強制
alias rm="rm -i"
時刻同期

(WSL2 を立ち上げたまま) スリープから復帰する (ことを繰り返す) と時刻が狂うらしいため、WSL2 起動時に時刻補正をするように .bashrc に記述する。この処理には 2 ~ 3 秒かかる。

echo 'synchronizing hardware clock ...'
sudo hwclock -s
clear

hwclock に対する sudo をパスワード入力なしで許可するため、sudo visudo コマンドにて /etc/sudoers を下記のように編集する。

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL
%sudo   ALL=(ALL) NOPASSWD: /usr/sbin/hwclock -s  #この行を追加

*1:この手の解説の省略が結構な罠。初回構築時にかなりの進捗阻害要因となるだけでなく、試行錯誤しているときは偶然に解決してしまったりして再現性がなく後にまた苦労する。当該環境構築メモを記録するのはこうした部分を補うため。

Functional Programming w/ C# LINQ

.NET 5.0 および C# 9.0 リリースによって LINQ 周りの機能が飛躍的に向上するかと思いきやそうではなかった。期待していた record の with 構文は式木にならず *1 IQueryable や LINQ to Entities で利用できないため、生産性向上にいっさい寄与しない *2

しかし、改めて LINQ 周りを調べてみると、わずかではあるが前進が見られるようだ。かつては IEnumerable 専用であった拡張メソッド TakeLast, SkipLast , Index 付き Select, Zip が IQueryable 対応となっている。

ちょっと残念なのは Zip が IQueryable * IEnumerable と第2引数に IEnumerable しか取れないことだ。まあ Zip のような演算をテーブル間で行う SQL 文法はなく、やるとしたらテーブルと持ち込んだデータ列との間でユーザが独自定義するものだろうという前提なのだろう。そこで IQueryable * IQueryable で Zip できないか拡張メソッドを定義してみた。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public static class ExtentionIQueryable {
  // IQueryable 版 Zip (IQueryable<T1> * IQueryable<T2> => IQueryable<ValueTuple<T1, T2>>)
  public static IQueryable<ValueTuple<T1, T2>> Zip<T1, T2>(this IQueryable<T1> seq1, IQueryable<T2> seq2) =>
    seq1.Select( (x, i) => new { Item = x, Index = i } )                                        // 第1引数シーケンスに Index を割り振る
      .Join(seq2.Select( (x, i) => new { Item = x, Index = i } ),                               // 第2引数シーケンスに Index を割り振る
            l      => l.Index,
            r      => r.Index,
            (l, r) => new { Value = new ValueTuple<T1, T2>(l.Item, r.Item), Index = l.Index } ) // Index をキーに Inner Join して値ペアを作る
        .OrderBy( x => x.Index )                                                                // IQueryable.Join() は順序を担保しないかもしれないため、ソートする
          .Select( x => x.Value );                                                              // Index を捨てる

  // IQueryable 版 Zip (IQueryable<T1> * IQueryable<T2> => IQueryable<U>)
  public static IQueryable<U> Zip<T1, T2, U>(this IQueryable<T1> seq1, IQueryable<T2> seq2, Expression<Func<T1, T2, U>> func) {
    var t   = Expression.Parameter(typeof(ValueTuple<T1, T2>), "t");                            // ValueTuple 型のパラメータを受ける式木
    var ti  = new [] { 1, 2 }.Select( i => Expression.Field(t, $"Item{i}") ).ToArray();         // ValueTuple の各要素を配列へ展開する式木
    var fnc = Expression.Lambda<Func<ValueTuple<T1, T2>, U>>(Expression.Invoke(func, ti), t);   // 引数配列を func に充て Invoke する式木

    return seq1.Zip(seq2).Select(fnc);
  }
}

IEnumerable 型の第2引数を受ける標準 Zip が2つのシーケンスを受けて値を組にして ValueTuple で返すものと、さらに演算の式木を受けて演算結果を返すものがあるため、それらに似せた。苦労したのは、2引数を ValueTuple で受ける関数とそのまま受ける関数との間の変換を式木で実現するところ。式木を解説する情報が少ないため、想像力で試行錯誤すること2時間(ここだけで所要時間の8割)。

コンパイルは通り、LINQ to Object を AsQueryable() したものを与えると正しい結果を IQueryable で返すため、理論上は IQueryable として機能しているはず。ただし、本当に LINQ to Entities として機能するかは、各 DB の .NET 5.0 用 QueryProvider のでき次第 *3。この点は別の機会に試験してみることとする。

【2021/02/24 追記】
構文は正しいようだが、残念ながら式木から SQL への変換で下記のようにランタイムエラーとなることから、QueryProvider が対応していないように見える。最もシンプルにして第2引数に Enumrable.Range() をとってオリジナルの Queryable.Zip(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2, System.Linq.Expressions.Expression> resultSelector) を試すもダメ。それどころかインデックス付き Select もダメ。結局、以前と変化なし。

System.InvalidOperationException: The LINQ expression 'DbSet<XXXXXX>()
    .Take(__p_0)
    .Zip(
        source2: __p_1, 
        resultSelector: (x, y) => new { 
            Property1 = x.Property1, 
            y = y
         })' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

*1:with 構文を式木で利用できるようにコンパイラ実装を修正すべきと C# 開発チームに提案したものの却下された。言語仕様上は問題なく、他の類似構文 (オブジェクト初期化子) の実装と明らかに非対称かつ不整合な不備であるため、論理的に問い詰めて食い下がってみた。が、めちゃくちゃ明晰な頭脳の持ち主である先方も最後は美しくない実装不備であることを認めたため、あきらめた。C# 開発チームには人口に膾炙する改善テーマが他にたくさんあり、各種演算子の式木対応は優先度がかなり低いようだ。

*2:with 構文は LINQ to Entities で利用できてこそ生産性を向上させる。LINQ to Object であればプログラマがちょっと気の利いた Clone() メソッドを書くだけで with と同等のことができる。右記参照 Functional Programming w/ C# LINQ - Crayon's Monologue

*3:Index 付き Select も IEnumerable と絡める Zip も SQL 的には難しくないため LINQ to Entities でも機能すると思うが、単純なことも意外とできなかったりするのが QueryProvider のため、過信してはいけない。

Training in Commercial Pilotage with FTD/FFS - Constant-Rate-and-Speed Climb / Descent, Lead Design for Level Off

羽田空港内某所で Flight Simulator による F/O 訓練。B737-Max で 2.0H。

本日は昨年 12 月の時に指導いただいた K 教官。Yoke の握り方や Elevator Trim Switch の触り方だとかに厳しい。正しく持たないと反応が遅れるし、まねごとではなくプロの方法を教えてもらうのは大変ありがたい。

Constant-Rate-and-Speed Climb / Descent & Climbing / Descending Turn

まずはいつもどおり RJTT RWY 34R から 6,000 ft 240 kt を目指す。5,000 ft まではいつもとは異なる指定 Climbing A/S、5,000 ft で HDG 100 への転針指示とあったものの、指定 HDG になることには A/S + 1kt、Alt ± 0ft, HDG ± 0° でピタッと Roll & Level Off させ、安定していますね~とお褒めいただく。

ここから Constant-Rate-and-Speed Climb / Descent の訓練開始。Level Off は VSI x 10% ft 手前から Power 調整開始、VSI x 5% ft 手前から Pitch 調整開始という前回の助言を活かすと、通常の Level Change はおおむね安定して Level Off でき、これまたお褒めいただく。

ただ一度だけ、Descnet で A/S が + 12 kt まで増速してしまっているのにこのタイミング法にやみくもに従ったため、Level Off がムチャクチャになってしまったことがあり、これを機に「Alt および VSI を Pitch で調整し、その上で出来上がる A/S を Power で調整する」のだと助言をいただく。ん? ということは Pitch → Power という操作順になり、前回の現役パイロット教官の助言と 180° 真逆の助言だぞと。しかし、おそらく前提条件の違いによるもので、どちらも間違っていない。その証拠に前回は Power を先、Picth を後にすることにより安定した Level Off になるということは理解できたが、うまくハマったのは基本の時だけで、A/S が目標から乖離しているときに開始点や操作スピードをどれだけ調整すればよいかは設計できなかった。きっと、操作タイミングの前後関係については前回の助言、操作量については今回の助言を取り入れ、体系立てて理論化すれば技量が飛躍的に向上するはず *1

応用編ということで訓練中盤は Turn を加えて Climbing / Descending Turn へ移行する。Turn を加えると、安定しているときは安定しているが、ダメなときはダメと出来栄えが少しボラタイルになる。調整対象が Alt と HDG の2軸となり複雑になるため、調整を体得できている (と感じる) シーンでは2軸であっても安定してこなせるが、「どちらへ操作するのだっけ?」と頭で考えているシーンではメタメタになっていると思われる。以前よりはだいぶんと技量向上したものの、瞬時に正しい方向へ調整するように身体が自然と反応するようにならなければならない。

Steep Turn

f:id:Crayon:20201025222010j:plain:right:w600同じ訓練科目ばかりでは飽きるでしょうと、訓練課程から外れてというか先行して、ジェットで初めての Steep Turn (Bank 45°) をやることに。Bank 角 30° を超えると "Bank Angle! Bank Angle!" の警報が鳴りやまない。ジェットエンジンは Power が操作に遅行するし大型機は慣性があるから、きっと操作はグダグダになるかと思いきや、教官に教わった

  • Pitch は 5° (同時に Bank は 45°) を常に維持
  • Pitch を維持することで Alt はほぼ維持できる (= Alt と VSI をスキャンして Alt を維持する Pitch を保つ) 一方で A/S が落ちてくるためそれを Power 操作して補う

という2点を気を付けていれば驚くほどきれいに Steep Turn が決まる。Pitch が維持できなければもちろん、Bank 角が 2° ズレても教官から指摘が入る。脇を締めて Pitch & Bank を維持する。Bank 45° で 5° ラインに Pitch を固定すると PFD 上の正方形のポインタがそろばんのように串刺しになるので、これをダイヤモンドと呼ぶらしい。この状態 (写真参照) の維持を目指す。ちょっとした他の Tips は下記の通り。

  • Bank 30° を超えるくらいから Power を入れ始める
  • Power 調整の目安は 5% 程度
  • Roll Out は Bank 角の半分くらいから始める ... これは Steep Turn に限らず、ジェットに限らず

Steep Turn でも、Pitch で Alt 維持、Power で A/S 調整というのが通念となっている。

本日の重要ポイント

  • (VSI でフゴイドの先を読みつつ) Pitch で Alt 維持、その上で出来上がる A/S を Power で調整、が基本
  • Steep Turn では脇を締めて Pitch 5° & Bank 45° を維持、Power 調整は 5% 程度
  • Pitch + Bank は常にウォッチし、Alt + VSI *2、A/S + Power、HDG は横目で瞬時に確認
  • Yoke は正しく握る、Elevator Trim Switch は使う時だけ指を遣る

*1:おそらく「Pitch = Alt 調整 → Power = A/S 調整」がいつでも (= 所望諸元との乖離が大きいときでも) 応用が利く調整の基本形、ただし、この手法では操作してから機体の反応を伺ってリアクションすることになり Stabilize までの所要時間が長くなるため、目標水準感がわかっている (Power = 63% で Level Flight になるなど) 、かつ、乖離が小さいとき (Descent Level Off の場合に目標 A/S + 2 kt 未満など) に微調整するために使う手法が「Power 先行 → Pitch 遅行」なのだと思う。

*2:おそらく VSI などは視線を遣らず、周辺視野で確認できるようになるとプロ水準なのだと思う。

LaTeX Notation in Blog

はてなブログLaTeX 記法ができることを知ったため、お試し。


どこかで見た記事によると $$ life = \int_{birth}^{death} study\ dt $$ だそうだ。$$ life = \int_{birth}^{death} study\ dt - \int_{birth}^{death} forget\ dt $$ の間違いではないかな。

Training in Commercial Pilotage with FTD/FFS - Constant-Rate-and-Speed Climb / Descent, Lead Design for Level Off

f:id:Crayon:20200925145809j:plain:right:w320羽田空港内某所で Flight Simulator による F/O 訓練。B737-Max で 2.0H。現役コパイロットである Y 教官からレクチャーを受ける。定期運航路線が減って現役パイロットも副業しなくてはならないのだろうか ...

羽田空港は沖留めこそ少なくなったが、ボーディングブリッジにつながれている機は多い。国際線ターミナルに Delta Airlines の B777 っぽい機が 2 機留まっているが乗客は少なく、旅客型に貨物搭載する Combi 運用となっているらしい。

Constant-Rate-and-Speed Climb / Descend

初対面である Y 教官と RJTT 34R から Take Off Climb し、いつものように 240 kt & 6,000 ft を目指す。お互いに慣れていないこともあって Climb Setting や機体重量がいつもと少し違うのか *1、A/S が伸びないなぁなんてぼーっと考えているうちに 5,000 ft 時点で A/S 203 kt, Pitch 16°, VSI 4,000 ft/min, Power 93% とメチャクチャになってしまう。 Pitch Too High & Power Too Much のため、当然 Overshoot する。これで Level Off Lead が苦手だと見抜かれたのか、本日のテーマは Climb / Descend での Level Off Lead になる。

前回の訓練で Autopilot が作る諸元を憶えたため、Climb / Descend (VSI 500 - 1,000 ft/min) での A/S, Pitch, VSI はスムーズに作れるようになった。しかし以前得たノウハウである VSI x 1/10 (ft) から開始している Level Off はうまいとはいえない。

そこで、今回、教官から教えてもらったコツは「ジェットでの Level Off は VSI x 1/10 (ft) で Power を先に調整し、VSI x 1/20 (ft) で Pitch を後に調整する」のが基本ということ。同時に実施すると操作がバタバタとなり安定しないことと、Power (の操作に遅行して出力) が調整されてくると Pitch 諸元も変わってくることが理由。さらに応用として、A/S が低ければ Power Up して速度を付けなければならないため Power 操作をより早めにする、逆に高ければ操作を遅めにというタイミング調整をする。

これが正鵠で、実践してみると驚くほどドンピシャに Level Off できる。VSI 500 - 1,000 ft/min での Level Off は完璧といわれるほど。Pitch はやや激しく調整することもできるため Power を操作した後でも十分に間に合い、Power → Pitch という調整は心に余裕をもたらす。この方法だと、これまで Climb Level Off よりも難しいように思えた Descent Level Off の方がより簡単にできる。

ということで、今度は実践の応用編。Idle Descent と N1 Climb をやってみる。A/S を 240 kt 一定にしつつ Power を Idle / N1 (Green Marker = 88% くらい) にして Descend / Climb する。Idle Descent では VSI 2,000 ft/min くらい出るため 200 ft 手前で Power 調整、100 ft 手前で Pitch 調整でピッタリ Level Off できる。N1 Climb では 2,500 - 3,000 ft/min くらい出る上に、一般に Δ VSI / Δ Power は上昇よりも下降の方が小さいため、Level Off がより難しい。 200 ft 手前ではまったく間に合わない。おそらく 300 ft 手前くらいから Power を一気に絞らないといけないのではないか *2。Alt の Trend Vector が調整開始のもう一つの目安になる、が、これを追っていては N1 Climb の Level Off はおそらくうまくできない。

よくよく考えると、VSI 500 - 1,000 ft/min の時の ΔPower は -28% (Descend) / +10% (Climb) であるが、Idle Descent / N1 Climb の時は -30% (Descend) / +25% (Climb) と通常の Descend / Climb に比べて ΔPower の増分が Descend で大きく変わらないのに対し Climb で 2.5 倍となっている。ΔPower を戻す分の操作量が Climb 側で相対的に大きくなっているのだが Throttle が重く戻すのに時間がかかることも勘案すると、当然 Climb では Lead を相当にとらなければならない。そしてまた一方で、これこそが Constant Rate-and-Speed Descent で Constant Speed とならずに増速してしまう理由でもある。Power Idle まで下げてもなお Power 過多なのだ。

というわけで Roll Off Lead は下表のような感じになるのではないかと推測する。(カッコ内数値は目標ではなく結果として到達するであろう水準。)

A/S Pitch VSI Power ΔPower Level Off Op. Start
Power Pitch
Climb N1 240 15.0-17.5 *3 (+5,000) 88% +25% -300 ft -150 ft
1,000 ft/min 240 5.00+ +1,000  73% +10% -100 ft -50 ft
Straight and Level 240 3.75  0  63% 0% *** ***
Descend 1,000 ft/min 240 2.50  -1,000  35% -28% +100 ft +50 ft
Idle 240 1.25  (-2,000) 33% -30% +200 ft +100 ft

本日の重要ポイント

  • Level Off は Power → Pitch という順に操作する
  • Level Off Lead は Power = VSI x 1/10 ft 手前, Pitch = VSI x 1/20 ft 手前で調整開始が基本
  • Power の調整開始点は A/S によって調整する (増速 / 減速する必要があるなら早め / 遅めに)
  • Idle Descent なら Power は 200 ft 手前で調整開始するくらいがよい
  • N1 Climb なら Power は 300 ft 手前で調整開始するくらいがよい
  • 調整開始のもう1つの目安は Alt (や A/S) の Trend Vector
  • VSI 1,000 ft/min との ΔPower の差分 (Throttle 調整量の違い) は、Idle Descent ではほとんどないが、N1 Climb では 2.5 倍

*1:確かに違うのだが、これは言い訳でしかない。

*2:ということが気になり始めると、Power を操作する前に EICAS を確認し、操作した後にまた EICAS を確認する、という以前の指摘は自然にできるようになる。

*3:当該 Power Setting で A/S 240 kt を維持する Pitch。