Training for First Officer w/ Flight Simulator

Level & Straight Flight

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Level & Straight Flight。

この一か月は多忙でイメージトレーニングできず、また本日は体調が悪いため、ステップチェックの水準には至らないと N 教官に予告しておく。「では、1時間だけいつもの通りの訓練をしたら、羽田・成田でトラフィックパターンでもしますか」と提案されるも、新しいことを始めたら操縦の感覚が狂うような気がして逡巡する。いつものメニューをだらだらとして過ごすのが今日のところはベストということにしておく。

K 教官と 1 時間 15 分ほどいつものメニューを基本としつつ、高度を 6,000 ft, 7,000 ft, 8,000 ft の間で変えてみたり、240 kts を 220 kts にしてみたり、重量を重くしてみたり、オートパイロットにして諸元修正のリードをモニターしてみたり、と少しだけ逸脱してみる。そのおかけでいろいろと発見あり。「今日は収穫があったな~、後でビデオを見て復習し、次回のステップチェックに備えよう」と思った矢先 ... N 教官から「さて、ではステップチェックしましょう、K 教官からエンドースが出ました」と。いやいやいや、今日はムリでしょうと思いつつも、まあダメもとでやってみる。

Take Off から 5,000 ft までは、Heading Deviation もなくきれいに安定して上昇する。5,500 ft から 6,000 ft までは、Power の適切なリードがいまひとつわからず 60 - 68 % くらいのレンジで上げたり下げたりして VSI が大きく揺らぐ。さきほど撮影したオートパイロットのリードを参考にして復習してからチェックに臨もうとしていたくらいであるため、Transition がまるで設計できていない。

5,000 ft 超えて 2 分以内に 6,000 ft で Stabilize、そのまま 2 分維持という目標に対し、x 分 xx 秒 *1 で Stabilize @ 6,000 ± 5 ft、240 ± 0.1 kts, Heading 337 ± 0。不合格かと思いきや「合格者の平均タイムの半分で優秀だった」と合格。体調が悪く今日はないなと思っていた Straight & Level Flight のステップチェックをクリアしてしまった。

本日の重要ポイント

  • VSI の針が Neutral Position にまとわりつくよう (= 針 1 本以内) に Deviation を抑える
    • ただし、操作の対象は Pitch であり、VSI を目標にしてはならない
  • Pitch 維持・調整には、ADI の中心点だけではなく Model Plane の Wing の上辺・下辺も含め近隣の Gauge の上辺・下辺に最も近い部分を効果的に利用する
  • Constant Rate Climb / Descend のスキャンは、ADI の Pitch ⇔ VSI (500 ft/min 未満は針、500 ft/min 以上は下辺の数値) の間を行き来する
  • 重量増加、低速の場合の安定姿勢はより Pitch Up になる、安定する Pitch を早めにつかむ
  • オートパイロットによる Transition は2段階で Intercept する
    • VSI レートが高いと目標高度への Intercept が難しいため、マニュアル操縦も 2 段階調整による Intercept をするとよい
    • あらかじめ、± 1,000 - 2,000 ft @ 500 ft/min などの穏やかな Transition のレートやリードを憶えておき、1 段階目はこのレートへ落とすことを目標にする

*1:ネタバレになってしまうため、タイムとクライテリアは非公開とする。

Functional Programming w/ C# LINQ

Python は好きになれない。

Python の長所と言われているものは、プログラミング初心者相手のごまかしであるか、あるいは C などの古い言語に対するアドバンテージであり、ライバルとなる他のモダン言語に対するものではない。オフサイドルールも弊害が大きくて嫌いであるが、一番の嫌いな点は、列挙 (Enumeration) の論理構成が直感に反し、思考がいちいち妨げられることだ。思考の順序と異なるために読みにくいし、書くときはカーソルを右に左に大きく動かさなければならない。

列挙の扱い方を C# (LINQ メソッドチェーン) と Python とで比較してみる。

1 から 10 までの整数のうち、偶数には 'Even', 奇数には 'Odd' を付してタプルを返す
  // C#
  Enumerable.Range(1, 10)
    .Select( x => x % 2 == 0 ? (x, "Even") : (x, "Odd") );

偶数なら 'Even'、そうでないなら 'Odd'。条件式 → 結果1 → 結果2 と並ぶ。

  # Python
  [ (x, 'Even') if x % 2 == 0 else (x, 'Odd') for x in range(1, 11) ]

'Even' を返すよ、偶数なら、そうでないなら 'Odd'。結果1 → 条件式 → 結果2と並ぶのは非常にみづらい。
なぜ、条件式を結果ではさむのか。

1 から 10 までの整数のうち、偶数のみを2乗して返す
  // C#
  Enumarable.Range(1, 10)
    .Where( x => x % 2 == 0 )
      .Select( x => x * x );

1 から 10 を対象として、偶数に絞り、2乗する。お題の指示の順序の通り。

  # Python
  [ x * x for x in range(1, 11) if x % 2 == 0 ]

2乗を返すよ、1 から 10 を対象として、ただし偶数のみね。うーん。なぜ、列挙を map と filter ではさむのか。
2乗を返すよ、から始めるならせめて、2乗を返すよ (map)、偶数のみね (filter)、1 から 10 を対象として (enumerate)、にならないものか。

ジャグ配列をフラット化する、フラット化して各要素を2乗する
  // C#
  var arr = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ];

  arr.SelectMany( i => i );                      // フラット化
  arr.SelectMany( i => i.Select( j => j * j ) ); // フラット化して各要素を2乗

SelectMany で要素を掴む i が IEnumerable<int> 型であることを意識すれば難しくない。

  # Python
  arr = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]

  [     j for i in arr for j in i ] # フラット化
  [ j * j for i in arr for j in i ] # フラット化して各要素を2乗

[ j for j in i for i in arr ] と書きたいところ。なぜ arr と要素変数 i, j がこのような並びになるのか。


Python は列挙を扱える点において古い言語よりアドバンテージがあるが、その文法は

  • 列挙走査 (for) は後置する (ただし、複数の for はネストの上位から下位に向かって並べる)
  • if は後置する (if - else 三項演算子は後置 if の語順を踏襲した発展形)

というようになっている。

Perl の if 修飾子を参考にしたのか、全体的に SQL の語順に似ているが ... と想像をめぐらすが、いずれにせよちぐはぐ感は否めない。列挙に対する map, filter, reduce 操作をスムーズに記述するだけの統合感・先進性には欠けており、思考を乱す順序でしかロジックを記述できない言語であると思う。データサイエンティストに好まれるというのが信じられない。

Training for First Officer w/ Flight Simulator

Level & Straight Flight

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Level & Straight Flight。

ADI での Pitch 維持を中心に据えることを心掛け、前回よりも安定するようになってきたが、まだ 2 分で安定させるに至らず。
今回は特に Tips なし。ただイメトレあるのみ。

Training for First Officer w/ Flight Simulator

Level & Straight Flight

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Level & Straight Flight。

前回つかんだコツを活かし VSI のトレンドを見てフゴイドを抑制するも Stabilize せず、想定していたよりもあきらかに調子が悪い。今日は Trim Wheel が回らず Pitch 感覚が狂ったのも理由の一つではあるが、それが主要因だとは思えない。集中してようやく Allowance ± 1 kt, ± 20 ft, ± 50 ft/min の幅に収まる程度、少し集中が途切れるとその倍くらいに簡単に振れてしまう。いつもは褒められる Bank も 1° ズレで Heading が最大 4 ° 揺らぐと元 ANA の K 教官に指摘される。夏の暑さで消耗し、集中力をやや欠いているものの、それでもなお Allowance に収まる程度の技量になってなければならない。

Allowance を逸脱するのは、Over Control が原因であることは自覚しているが、それを抑える方法がわからない。そこで Yoke をホールドしたまま N 教官のデモ操縦をトレースし、その後、教官とディスカッションする。指摘されたのは、私の操縦では、操作した後、機体が反応する前に早々に「反応しない」と判断してもっと踏み込む追加操作をしている、ということ。自覚あり。実際にすべきは「じわじわとゆっくり操作」し、その後、機体が所望方向に動き出すまで「待つ」、そして所望方向に動き出したら追加操作をむしろ「半分戻す」方向に行うということ。そして半分戻し後に安定維持させる Pitch 水準をあらかじめ見積もっておく。4 つの気づきあり。

  1. Pitch 操作でも当て舵のような「半分戻し」操作が必要
    • 目からうろこ。いまさらながらの気づきだが、言われてみれば当たり前。Aileron を傾け Bank するのと同様 Elevator を傾け気流を曲げて Pitch を動かしているのであり、当て舵・半分戻ししないと、静止状態から動かし始めたのと同程度の強い力を受け続け、曲げた方向にどんどん傾いてしまう。
  2. VSI のみで調整せず、操作目標を Pitch 水準におく
    • VSI は参照するものの、操作の目標はあくまで Pitch 水準を安定させることであり、その結果として VSI = 0 となる。
  3. 安定維持させる水準をあらかじめ見積もっておく
    • Stabilize するであろう Pitch そして Power をフゴイドで行きつ戻りつする間にも見定めておく。
  4. 脇を閉めたり、肘掛けを使って、ニュートラル・ポジションを感知できるようする
    • いままで脇が甘い中でよくぞこれだけ繊細なコントロールができたと逆に褒められた。

VSI は見なくても操縦できると、最後に VSI を隠して Partial Panel Instrument Flight する。すると Yoke 操作が慎重になり、いい塩梅に。これまでいかに VSI に振り回され過剰に反応しすぎていたことか。

本日の重要ポイント

  • 操作は当て舵・半分戻しする
  • 操作目標は VSI のトレンドではなく Pitch 水準とする
  • 安定維持する Pitch, Power 水準をあらかじめ見積もる
  • 脇を閉めてニュートラル・ポジションを作る

Functional Programming w/ C# LINQ

ここ 9 ヶ月ほど C# LINQ プログラミングをしてきて、関数型・集合論的演算はやはり素晴らしいと思ってきたが、コーディングの目的が調査・研究から運用目的のシステム構築に移りつつあるためか、LINQ の限界が見えてきた。

C# は unsafe で書けば C / C++ 級の低水準コーディングが、managed で書けば Java 級の高水準コーディングが、LINQ で書けば超高水準な関数型・集合論的コーディングが可能になり、可読性と開発効率が飛躍的に向上する。水準を選べるその幅広さと、OnMemory・RDB へのデータアクセスが透過的・無差別に記述できる LINQ という他の言語にない OR マッピングアプローチがすばらしいのだが……… unsafe → managed → LINQ と高水準になる過程で抽象化が図られているわけであり、抽象化とは実装を切り離すこと、実装はコンパイラあるいはランタイムが担い、プログラマが管理しなくてよい (≒できない) とすることであることを再認識した。

このことに思いを致した結果、「実装を切り離す」ということの弊害が目につくようになってしまった。

LINQ を多用してきたものの .GetEnumerator() は2、3度ほどコピペでしか書いたことがないため見過ごしてきたが、想定通りに動かないケースに対応しようと先週、ある .GetEnumerator() を独自に改良した。そして、LINQ を使い始めて 10 年になるが、ここにきて初めて IEnumerable<T> が内部でどのように動作しているのか理解した。

IEnumerable<T> は OnMemory でも RDB でも同じ記法で透過的に記述できる魔法だが、その名のとおり、シーケンシャルアクセスしか定義せず、ランダムアクセスを用意しない。n 番目の要素を取得するには .ElementAt(n) とするが、これは配列のインデクサアクセス array[n] とは異なり、.Skip(n).First() と同じ *1であり、実際に LINQ は .ElementAt(n) が呼ばれるたびに、.GetEnumerator() して列挙子を取得し、先頭から n 要素スキップして最初の要素を返す、という気の遠くなるような手順を踏む。

これをインデクサと同じくランダムアクセスとするようショートカットする (か否かを決める) のは、IEnumerable<T> インターフェースを持つデータ構造 *2 の実装の方であり、IEnumerable<T> インターフェース自体は我関せずという態度なのだ。また、いまのところ、C# コンパイラ .NET ランタイムも IEnumerable<T> やその実装の内部の挙動を最適化しない。

OnMemory データ操作も RDB データ操作も記述を可能な限り統一しようとしてきたが、実装の違いがある以上、実行効率とのトレードオフを勘案すると限界がある。LINQ は可読性を高めるが、やりすぎると却って可読性が落ちることも経験上わかっている。

LINQ の使いどころはやや控えめに考えることにし、C#LINQ 最適化チューニングするまでは脱 LINQ 至上原理主義を宣言しようと思う。

*1:LINQ to Entities で RDB アクセスするにはこの記法にする必要がある。

*2:固定長構造の配列やリストならランダムアクセスが可能だが、FileStream などの可変長構造ならランダムアクセスは不可能である。

Training for First Officer w/ Flight Simulator

Level & Straight Flight

都内某所で Flight Simulator による F/O 訓練。B737-800 で 2.0H。

いつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Level & Straight Flight。5,000 ft 通過後 2 分以内に 6,000 ft, 240 kts で Level Off & Stabilize、その後 2 分間 ± 20 ft, ± 1 kt を維持するのが規定 *1

前回の反省から、各ポイントポイントで Pitch の目標水準を維持することで、スムーズに 6,000 ft, 240 kts へ到達する。しかし、その後が安定せず、収束したり発散したりする。教官から、3点ほどダメ出しをいただく。

  • Throttle 調整時に N1 を凝視し、その間に Pitch が大きく逸脱する
  • Throttle 調整後の Power Settings を確認していない
  • Bank 調整時に ND にやや集中しており、その間に Pitch が少し逸脱する *2

凝視 (= 高頻度 Scan) すべきは PD の Pitch であり、N1, ND は凝視せず、チラ見すること。Throttle 操作、Bank 操作は前後に水準 (左右バランスを含む調整量) の確認を必ず行うこと。操作後確認のタイミングは、Throttle の場合はエンジン音が安定収束したら、Bank は 5 秒間隔で、とのこと。Throttle も数 % の操作なら 5 秒後確認、片肺のみを利用した微調整もありだと思った。

前回、諸元コントロールは、Pitch 目標を定めて一定枠内 (最大で ± 1.25°) に抑えることで行う、ということを学んだ。それにより Transition はおおむねうまくいく。しかし、それだけでは Stabilized Level Off はしない。今回学んだのは、Pitch を一定枠内に収めたら、次はさらに繊細な VSI を一定枠内 (最大で ± 150 ft/min) に収め、その振れ幅を徐々に小さくすべく Pitch 髪の毛 1 本精度でフゴイド運動を収束させる、ということだ。Transition から Level Off へ移るにあたり、操縦は Pitch を枠内へ収めるゲームから VSI を枠内へ収めフゴイドを制するゲームへ変わる。下表のように、フゴイドの揺れ幅を小さくしつつ、必要に応じて揺らぎの中心をずらし、所望 A/S, Alt で安定させる。

Action Scanning Objective Allowance
Step 1 Transit 水準 x Pitch の水準 Target Pitch ± 1.25°
Step 2 Level Off トレンド \frac{dx}{dt} A/S のトレンド Target A/S ± 1 kt
Alt のトレンド Target Alt ± 20 ft
VSI の水準 Target VSI ± 150 ft/min
Step 3 Stabilize トレンドのトレンド \frac{d^2x}{dt^2} VSI のトレンド Target VSI ± 0 ft/min

本日の重要ポイント

  • Pitch Control
    1. Pitch を枠内 (最大で ± 1.25°) に収めることで Transition の諸元コントロールを行う
    2. VSI *3 を枠内 (最大で ± 150 ft/min) に収めることで Level Off する
    3. VSI のトレンドを相殺して VSI = 0 とし、フゴイドを収束させることで Stabilize させる
    4. 操作目標は変動の検出感応度が低い順 (Pitch → A/S → Alt → VSI)
  • Power & Bank Control
    • Scan の中心は PD (Pitch)、N1 (Power) や ND (Bank) はチラ見してすぐ PD (Pitch) に戻る
    • Throttle 操作、Bank 操作の前後に水準 (調整量) の確認を必ず行う
    • 事後確認は 5 秒後、エンジン音安定後
    • Throttle の左右調整も忘れず、ただし、一時的な微調整は片肺で実施することもあり

*1:規定諸元にピタっと合わせることが目標なのではなく、ズレをいち早く感知していち早く修正することを維持し、乖離の収束を継続できるかが目標であり、試験官はそこを見ている。

*2:おそらく Yoke を右に切るときに Pitch Up、左に切るときに Pitch Down の傾向があると思われる。

*3:VSI は ±400 ft/min を超えると白字で数値が表示される。

Functional Programming w/ C# LINQ

ここ 8 ヵ月ほど C#LINQ でちょっとばかりデータサイエンティスト的なことを やっている。

大量データ処理は C# LINQ が群を抜いて優秀である。

LINQ はクエリ式より複雑な取り回しをできるメソッド式で記述する方が便利であることが多いが、ここにきてクエリ式の方が有利になるケースを発見した。非決定計算の組合せ総当たりはメソッド式よりクエリ式がかなり効果的だ。

C# ver 8 の LINQ 両方式で、有名な覆面算 "SEND + MORE = MONEY" となる数字 (S, E, N, D, M, O, R, Y) の組合せを総当たりで求めてみる。

#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

/// 非決定計算
namespace Test {
  public static class TestX {
    public static void Main(string[] argv) {
      var sw = new Stopwatch();

      sw.Start();

      foreach (var item in argv[0] switch { "2" => Money2(), _ => Money1() })
        Console.WriteLine(item);

      sw.Stop();
      Console.WriteLine(string.Format("{0:##,##0.0} ms", sw.ElapsedMilliseconds).PadLeft(8));
    }

    public static readonly IEnumerable<int> Digits = Enumerable.Range(0, 10);

    /// クエリ式方式
    public static IEnumerable<dynamic> Money1() =>
      from s in Digits                                        where s != 0                  // クエリ式は、各 from 行で文字の値候補を生成
      from e in Digits.Except(new [] { s                   })
      from n in Digits.Except(new [] { s, e                })
      from d in Digits.Except(new [] { s, e, n             })
        let send  = new [] { s, e, n, d    }.Aggregate( (x, y) => x * 10 + y )              // 必要候補が確定したら計算を行い、無駄に重複させない

      from m in Digits.Except(new [] { s, e, n, d          }) where m != 0
      from o in Digits.Except(new [] { s, e, n, d, m       })
      from r in Digits.Except(new [] { s, e, n, d, m, o    })
        let more  = new [] { m, o, r, e    }.Aggregate( (x, y) => x * 10 + y ) 

      from y in Digits.Except(new [] { s, e, n, d, m, o, r })
        let money = new [] { m, o, n, e, y }.Aggregate( (x, y) => x * 10 + y ) 

          where send + more == money
            select new { send, more, money };

    /// メソッド式方式
    public static IEnumerable<dynamic> Money2() =>
      Digits
        .SelectMany( s => Digits.Except(new [] { s                      })
        .SelectMany( e => Digits.Except(new [] { s, e                   })
        .SelectMany( n => Digits.Except(new [] { s, e, n                })
        .SelectMany( d => Digits.Except(new [] { s, e, n, d             })
        .SelectMany( m => Digits.Except(new [] { s, e, n, d, m          })
        .SelectMany( o => Digits.Except(new [] { s, e, n, d, m, o       })
        .SelectMany( r => Digits.Except(new [] { s, e, n, d, m, o, r    })
        .Select    ( y =>               new    { s, e, n, d, m, o, r, y }))))))))           // メソッド式は、ここで組合せ生成
          .Where( _ => _.s != 0 && _.m != 0 )                                               // 全候補が確定してから non zero の絞り込み
            .Select( _ => new {                                                             // 各文字の直接参照はできず _ でオブジェクト参照
              send  = new [] { _.s, _.e, _.n, _.d      }.Aggregate( (x, y) => x * 10 + y ), // 全候補が確定してから計算
              more  = new [] { _.m, _.o, _.r, _.e      }.Aggregate( (x, y) => x * 10 + y ),
              money = new [] { _.m, _.o, _.n, _.e, _.y }.Aggregate( (x, y) => x * 10 + y ),
            } )
              .Where( _ => _.send + _.more == _.money );
  }
}

クエリ式は各 from 行で文字の値候補を生成し、必要な個所で条件抽出・計算を逐次行うことが可能なため、重複計算や組合せの広がりを最小限に抑えている。

メソッド式はネストを深く SelectMany() を連ねてから Select() することで一次元シーケンスを作成するしかなく、結果として組合せ生成をしてから条件抽出し、計算せざるを得ない。当然、性能も悪くなる。また、オブジェクト参照の _. がうっとおしい。

クエリ式も場面によっては使える。