Training in Commercial Pilotage with FTD/FFS - Constant-Rate-and-Speed Climbing / Descending Turn

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

Constant-Rate-and-Speed Climbing / Descending Turn

まずは K 教官といつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Straight & Level Flight。ちょっと集中力を欠いて Level Off で Overshoot する。

その後、本日の訓練目標である Constant-Rate-and-Speed Climb / Descend。それなりに滑らかに Climb / Descend できる。前回指摘された Scanning のコツを活かし Heading Deviation はほとんどなく、VSI Rate も安定して操作できるものの、A/S Deviation は ± 5 kt ほどズレてしまう。おそらく Throttle 調整のタイミングと量の判断が遅いため (特に本日は頭の回転が鈍いため) だと思われる。

本日は Constant-Rate-and-Speed Climb / Descend の訓練に終始すると思っていたら、今度は Turn を加えて、 の訓練に入る。当然だが Turn が入ると Smooth Transition はさらに難しくなる。Roll Out Lead は Turn と Climb / Descend が重なっていようがいまいが、それぞれまあうまくこなすが、Bank を傾けた時に、1) 適正な Bank を維持する -- 当て舵が重要、2) 適正な Alt と VSI を維持する -- Pitch が重要、3) 適正な A/S を維持する -- Throttle が重要、4) Altitude Level Off Lead を考える、5) Heading Roll Out Lead を考える、を同時に実行するのはやはり難しい。2), 3) はもとから難しいが、Turn が加わると 1), 5) に思考を取られる分、さらに難しくなる。

MCP Operation

f:id:Crayon:20200831153513j:plain:right:w480本日は夏バテと睡眠不足のため、2 時間全部を集中力が必要なマニューバ訓練に充てるのは難しいことは初めから自覚していた。近々、Microsoft Flight Simulator 2020 を購入することから Autopilot Mode Control Panel (MCP) の操作方法を熟知したいということもあり *1、後半戦はなんとかマニューバ訓練を切り上げ、MCP 操作を教えてもらおうと予め画策していた。そうしたら図らずも、教官も Constant-Rate-and-Speed Climbing / Descending Turn 訓練の一環で諸元を理解したほうがよいだろうと思い立ち、MCP を弄って Autopilot の諸元を参考にしようということになる。

Autopilot の諸元は驚くほど滑らかで (特に Throttle の) 操作量がきわめて少ない、が、手動で操作したらこの操作量ではすまないであろう。そこは少し割り引いて (というか割り増して) 考える必要がある。ビデオを見返し、後日まとめた諸元は下表のとおり。


A/S Pitch Bank VSI Throttle Note
水準 増減 水準 増減
Climb 240 5.00+ +2.00 +1,000 73% +10%
Straight and Level 240 3.75 0.00 0 63% 0%
Descend 240 2.50 -1.25 -1,000 35% -28%
Climbing Turn No Data
Turn 240 5.00- +0.60 30° 0 65% +2%
Descending Turn 247 2.50- -2.00 30° -1,000 33% -30% +7 knot *2

本日の重要ポイント

  • Constant-Rate-and-Speed Climbing / Descending Turn は以下を同時に考える
    1. 適正な Bank を維持する -- 当て舵
    2. 適正な Alt, VSI を維持する -- Pitch (Pitch は適正水準の数値を憶える)
    3. 適正な A/S を維持する -- Throttle (Throttle は適正水準の数値を憶える)
    4. 適正な Altitude Level Off Lead をとる (Lead は適正水準の数値を憶える)
    5. 適正な Heading Roll Out Lead をとる (Lead は適正水準の数値を憶える)

*1:12 年前 Microsoft Flight Simulator X の MCP と FMS (と飛行諸元) をいま一つ理解できなかったことが実機で訓練してみようと思ったきっかけであり、結果としてパイロットライセンスを取得したことにつながっている。ディープな世界を独学するのは難しい。

*2:Turn よりも Pitch と Throttle を下げているため、増速している。

Training in Commercial Pilotage with FTD/FFS - Acceleration/Deceleration, Constant-Rate-and-Speed Climb/Descent

羽田空港内某所で Flight Simulator による F/O 訓練。B737-Max で 2.0H。f:id:Crayon:20200711105104:plain:right:w480訓練前にターミナルビル1階の航空神社にお参りしたご利益か、今日は実り多く、また、最初から操縦が非常に滑らかかつ安定的で終始「練度が高い」感を醸し出す、充実の一日となる。

Take Off Climb Step Check

Take Off Climb を少し練習した後にすぐに N 教官の Step Check へ進むも、練習初回が最も上手に行く。B737-Max の諸元をまだよくつかんでいないからこそ、「諸元の水準を合わせればなんとかなる」的な雑なリード設計から脱して、少し動かしてはその反応を見て操作必要量を探りつつ所望諸元に近づけるようこまめに当てに行く Feedback & Reaction が上手にできるようになっている。

Acceleration / Deceleration

次の訓練科目 Acceleration / Deceleration へ進む。常に Alt 6,000 ft を維持しつつ増減速する。最初に A/S 240 kt から 200 kt へ減速し安定させたら、 240 kt へ増速し安定させる。次に Configuration Change (Gear Down & Flap Down) を伴う140 kt への減速、つまりは Slow Flight へ移行する。低速域でただでさえ難しい制御が Configuration Change で Drug が増えさらに難しくなる。しかしそこはセスナの実機操縦で得意だった Slow Flight、逸脱を小さく安定させる。再度 240 kt へ増速したら、 N 教官から「最後はお遊びで 120 kt まで落としてみますか」との指示で Super Slow Flight へ移行。A/S をちょっと下方逸脱すればすぐ Stall する、加えて機位は群馬・新潟県境の山岳地帯、Alt をちょっと下方逸脱すればすぐ CFIT になる、というすべてが超スレスレの Risky Flight。A/S が 118kt くらいになると A/S Gauge が黄色くなり Stall Warning *1 が鳴るが、逸脱を最大でも A/S -5 kt, Alt -500 ft, Heading 25° くらいに収め、急な Attitude Change をしないよう、Bank を傾けすぎないよう操作して安定させる。低速域だと TO/GA くらいに Power Set しないと Alt の損失を埋めるのに至らない。

A/S 140 kt w/ Configuration Change は難しいが「よく考えるとこれは Final Approach ですね」と教官に振ると、Pitch を下げて Descent している Final Approach と比べて、Straight & Level @ 6,000 ft は Pitch Up しないとできないため、Drug がより強く発生しており、それだけより難しいとのこと。この過程で気づいたことがいくつか。

  • Alt & A/S を Power だけで調整はできない
    • 当たり前だが Pitch でも Alt & A/S は変わる。Pitch と Power で調整する。
  • 探りながら細かく調整する、でよい
    • Next Action は調整の結果を見極めてからでよい。設定を変えたら、高度安定するまで待つ、速度安定するまで待つ、エンジン音周波数が安定するまで待つ。
  • Power Setting の調整は、Climb では易しく、Descent では難しい
    • Power は Idle (Min) まで下げても高度がなかなか下がらない一方、(極端な低速域でもない限り) Power を To/GA (Max) まで上げなくても高度は上がるため、所望高度での Level Off における Power 変量は Climb で 少なく、Descent で多くなる。(2,000 ft 移動して 6,000 ft 目標の場合、それぞれ 73% → 58%、23% (Idle) → 58% くらい。)
    • Power 変量が多ければ Attitude Change が大きくなる。妥当な Attitude から逸脱すれば Drug が増え、Feedback が狂って Overshoot し、調整が収束しにくくなる。必然的に、調整は一気にはできず、長くこまめに行うことになる。
    • Alt & A/S は少し超過気味に調整しておいて、Pitch 操作で Drug を作って微調整するという方法もありそう。特に、低速域・低高度でのAlt や A/S は、超過は許容、不足は非許容とするのがよい。
  • Pitch の微調整を Trim でやってはいけない
    • これはよく言われるあるあるだが、今回初めて実感として理解できた。安定的に Straight & Level している最中ならばまだ許されるかもしれないが、Power Setting Change, Configuration Change, Gust 等々の Pitch を大きく動かすファクターがある中で Trim で Pitch 調整をやってしまうと、どこが Pitch の妥当な水準か簡単に見失う。実際、今回、Trim でちょっとした Pitch Up を図ったところ、おそらくその時に教官が Flap Down したのであろう、簡単に Pitch が 5° ほど Alt が 1,000 ft ほど跳ね上がってしまった。

Constant-Rate-and-Speed Climb / Descent

最後の訓練科目 Constant-Rate-and-Speed Climb / Descent を実施する。まず 1,000 ft/min で 10,000 ft まで Climb してみる。Constant-Rate-and-Speed であっても、フゴイドを勘案した上である VSI を基準点 に Pitch を作ることは Straight and Level と同じ、VSI x 1/10 (ft) 程度の Lead を取って Level Off する、と以前に学んだことを活かし、そのとおりに実演してみる。次に 500 ft/min で 8,000 ft まで Descend する。こちらでも、Descend では Level Off Lead を少し大きめにとる、と以前の知恵を活かす。

途中、教官に1回 Pause され「Heading の修正が遅い」と指摘される。反省すべき点は 2つある。

  • Scanning が遅い、そして、Bank を調整すべき方向を確認していない
    • Pitch & Power → A/S & Alt の修正が大変であるためこちらを優先し、Bank → Heading は劣後させている自覚がある。
    • Heading の状況に関わらず、とりあえず Bank Neutral にして (おけば Heading がそれ以上ズレないため) 余力が生まれたときに転進しようと考えている。
    • Bank 調整をする前に、まず ND で修正すべき方向を確認する *2
  • Pitch を調整すると Bank も揺らぐ、が、それを修正していない
    • Pitch も Bank も Yoke 操作であるため、片方のみを意識して調整したら、もう片方がズレるのは当たり前。常に両者一体として調整する。
    • 教官にヒントをもらって体得したが「Pitch を直したら Bank も見る、Bank を直したら Pitch も見る」とすると驚くほどブレなくなるため、これをクセにするとよい。

f:id:Crayon:20200712113233j:plain:right:w480問題が複雑であることから Yoke (Pitch) と Throttle 操作で A/S + Alt (+ Power) という2次元の調整と Yoke (Bank) 操作で Heading という1次元の調整をあえて分離していたのだが、すべては右図のとおり連動しているため、また PD 上の Center Point があるべき位置は上下 (Pitch) 軸+左右 (Bank) 軸上の1点にあるはずであるから、 Pitch と Bank はセットとして修正し3次元で調整しなければならない*3ということだ。

最後に 500 ft/min で Climb してから、さらに 750 ft/min での Climb を指示され、750 ft/min Climb の Pitch を探っていると ... 教官にまた Pause され「なぜ、そんなに手探りで探る?」と問われる。つまりは、1,000 ft/min Climb と 500 ft/min Climb を経験しているのだから、750 ft/min Climb の Pitch は、全く手探りではなく、当たりがつくはずだということを問われているのだ。むー、難しい ... 私の Short-Term Memory は大きくないのよ ... さきほどの Pitch がどのくらいだったかなど憶えているわけがない。しかし、そういう観点で設計ができないのは、先の信号が赤ならばエンジンブレーキを効かせつつ惰性で進むか緩やかにブレーキをかけるだけでよいのにアクセルを吹かしてしまうタクシーのようなものだ。乗客に G を感じさせず、エコ操縦し、ついでに訓練時間も短くすます、技量の高い操縦士になるには、先々を見据えたリード設計ができるようにならなければならない。自他ともに認めるほど技量が一段階向上したことがあり、次の目標水準ができたということだ。

本日の重要ポイント

  • A/S & Alt は探りながら細かく Pitch & Power で調整する
  • 設定を変えたら、高度安定するまで待つ、速度安定するまで待つ、エンジン音周波数が安定するまで待つ
  • Power Setting の調整は、Climb では易しく、Descent では難しいため、Descent では特に早期・入念にリード設計する
  • Power 変量が多い調整は、一気にやろうとせず、長くこまめに行う、また、A/S & Alt を超過気味にしてから Pitch 操作で Drug を作って微調整することを視野に入れる
  • Pitch の微調整を Trim で行わない
  • Pitch を直したら Bank も見る、Bank を直したら Pitch も見る
  • Bank 調整をする前に、まず ND で修正すべき方向を確認する
  • 経験した飛行諸元を憶え、それを活かして先々を見据えたリード設計をする

*1:Stick Shaker ではない。

*2:教官に指摘されたが「Bank が傾いていて Heading が所望する方向へ流れているのに、わざわざ Bank Neutral にしてしまうこともある」とのこと。

*3:飛行諸元の結果として合わせるのは3次元 (A/S, Alt, Heading) だが、飛行諸元の微小変量・感応度 (Trend Vector, VSI, Pitch, Bank) と出力 (Power) も合わせないと結果の3次元を維持できないから、結局は8次元を所望諸元に合わせることになる (Trend Vector と Power は常に見ているわけではないため、実質 6.5 次元くらいになる)。

Training in Commercial Pilotage with FTD/FFS - Lead Design for Take Off Climb w/ B737-Max

羽田空港へ移転した某所で Flight Simulator による F/O 訓練。B737-Max で 2.0H。f:id:Crayon:20200614100048j:plain:right:w480

Take Off Climb w/ B737-Max

3 月に実施予定だったものの移転とコロナ騒動で何度もリスケ・延期してようやくめぐってきた、モーション付き B737-Max 新シミュレータによる初訓練。といってもモーションは自分では動かさず、遊びに来た他のお客の体験操縦を見ていただけだが。モーションなしでは FTD (Flight Training Device) だが、ありでは FFS (Full Flight SImulator) となる。

前半は N 教官と、後半は K 教官といつものとおり Take Off from RJTT RWY 34R から 6,000 ft, 240 kts での Straight & Level Flight。B737-Max シミュレータは B737-800 シミュレータと全然違う! シミュレータ自体の感触の違いとしては以下の通り。

  • York
    • York は重くなったが、実機に近くなったらしい。
    • 遊びがなくなって反応がよくなった。以前のものはニュートラル近辺では小さく動かしても感応せずグイっと大きく動かさなければならなかったが、初動にも敏感に反応するようになった。ただし重い。
  • Throttle
    • Throttle も重くなり、これは実機よりも重いらしい。
    • 静止摩擦係数が小さくなって微調整がやりやすくなった。以前のものはグイっと力を込めないと動き出さなかったが、滑らかに動き出すようになった。ただし重い。
  • PFD, ND, EICAS のディスプレイ
    • 高解像度になり見やすくなった。
    • PFD が最大 8 秒くらい固まることがある。(景色は操縦に遅延なく追随するため、グラフィックボード ~ PFD 出力系統のどこかがボトルネック。)
  • その他
    • Bank がかなり敏感になり、Wing Level で安定せず常に Roll 調整が必要になった。実機よりも敏感らしい。

B737-800 と B737-Max との間の機種の違いとしては以下の通り。

  • EICAS は PFD に近づき、視点移動を要さなくなって見やすくなった。
  • Throttle 出力がかなり強くなった。Power 諸元は B737-800 と異なるため、見直しが必要になった。

Max はかなり高出力であるため、諸元を全面的に見直す必要がある。今回やってみて機種移行訓練というものがなぜ必要か実感としてわかった。今回のテーマは B737-800 から B737-Max への機種移行訓練だ。

まず、Straight & Level はいままでの Pitch = 2.5° +, Power = 63% では通用せず、Pitch = 3.8° (2.5° と 5° の中間か気持ちやや上), Power = 57% くらいにしないと維持できない。旧機種で Check を通った身としてこのくらいはやってすぐに感覚で把握する。フゴイド運動することを見越して、VSI の揺らぎを先読みし、Pitch を安定させる。

しかし、Throttle のリード量調整を把握できていないため Altitude Transition はなかなか滑らかにいかない。Alt を 100 ft オーバーシュートするのはまだいい方で、 A/S を 25 kts もオーバーシュートすることまである。何度か Take Off & Climb to 6,000 ft をやってみて気づいたのは、高出力であるため、Take Off Climb では Pitch をいままでより高め (+1目盛り 2.5°) に設定して A/S を維持する (制限速度 250 kts を超過しない!) こと、1,000 ft to Level Off あたりに近づいたら早めに Power を絞ること。また、Descent では Power Idle くらいにまで下げないと降下しない、ということ。

そんなこんなで最後には Take Off Climb → Straight & Level Flight 移行のタイムチェックも 2 分程度に収まりそうなくらいの水準になったため、Take Off & Climb の諸元はこのくらいになるのではないかという想定値で更新 (黄色マーク) して *1、次回 Stage Check に備える。


B737-MAX A/S Pitch Alt VSI Throttle
Rotate VR 0 0 TO/GA
Rotate + 5 sec 12.5
400 ft 159 20.0 400 4,000-
3,000 ft 240 12.5 3,000 3,000-
1,000 to Level Off 10.0 5,000 2,000- 75%
500 to Level Off 5.0+ 5,500 1,000- 65%
Level Off 3.8 5,950 500- 57%

本日の重要ポイント

  • Max は高出力のため、Pitch は高い方向へ、Power は低い方向へ諸元を見直し
  • Descent は Power Idle まで下げてもよい
  • フゴイド運動することを見越して、VSI の揺らぎを先読みし、Pitch を安定させる

*1:実感としてわかったのは、エンジン性能により、安定する Pitch も変わるということ。DC-10 がメーカー推奨よりも Pitch を大きく取った方がエネルギー効率がよいと航空会社が気づいて 4° Pitch で巡航するようになり、他機種よりも通路が傾いているため「DC-10 乗務は (なぜか) 疲れる」と CA がボヤくエピソードを思い出した。

Functional Programming w/ C# LINQ - with 初期化子の代替メソッド定義 for C# 8.0 or former

C#LINQ を使ってデータ変換をしていると、データクラスのインスタンスをコピーして一部のプロパティを別の値に差し替えたいということがよくある。C# 9.0 では Records v2 の with 構文というものが導入されるようで期待できるのだが、現行の C# 8.0 では存在しないため、Clone() メソッドを作成し、リフレクションでプロパティを置換していた。

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

public abstract class Record : ICloneable {
  /// <summary>
  /// オブジェクトのクローンを Shallow Copy で作成する。
  /// (ICloneable 互換)
  /// </summary>
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
  public object Clone() => Clone(null);

  /// <summary>
  /// オブジェクトのクローンを Shallow Copy で作成する。
  /// 引数で指定される匿名クラスで同名プロパティの値を置き換える。
  /// </summary>
  public object Clone(object? param = null) {
    var clone = this.MemberwiseClone();
    var subst = param ?? new {};
    var props = subst.GetType().GetProperties();
    var dic   = clone.GetType().GetProperties()?.ToDictionary( p => p.Name, p => p);

    foreach (var p in props)
      if (dic?.ContainsKey(p.Name) ?? false)
        dic[p.Name].SetValue(clone, p.GetValue(subst));
      else
        throw new ArgumentException($"{this.GetType().Name} のクローン時にプロパティ {p.Name} の置換に失敗しました。\n" +
                                    $"当該プロパティは {this.GetType().Name} に存在しません。");

    return clone;
  }
}


下記のように使う。この例ではプロパティ数が少なくてありがたみがないが、プロパティがたくさんあるデータクラスをコピーしてごく一部のプロパティしか置換しない場合に (さらに Select, Join, GroupJoin 等で何度も何度も変換する場合には特に) 重宝する。

public class RecordSample : Record {
  public string ID    { get; set; }
  public int?   Value { get; set; }

  // list_original を基にしてプロパティ Value の値を2乗にしたもうひとつのシーケンス list_converted を作成する例
  public static void Test() {
    var list_original =
      Enumerable.Range(0, 10)
        .Select( x => new RecordSample {
          ID    = "" + x,
          Value = x     ,
        } )
          .ToList();

    var list_converted =
      list_original
        .Select( org => org.Clone(new { Value = org.Value * org.Value }) as RecordSample ) // ← ココで使用
          .ToList();
  }
}


これまで、毎度毎度リフレクションでプロパティ走査をするのは非効率であることは自覚しつつも、このメソッドに特に不満はなかった。が、しかし、最近 PostgreSQL サーバをたてて大量データを扱うようになり、このオーバヘッドが気になったため、この記事を参考に式木を使ってリフレクションを極小化してみた。

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

using static System.Linq.Expressions.Expression;

public abstract class Record : ICloneable {
  private static readonly IDictionary<string, Action<object, object>>DicAction = new Dictionary<string, Action<object, object>>();

  /// <summary>
  /// プロパティの型をチェックする。(静的メソッド)
  /// </summary>
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
  private static bool CheckPropertyType(Type dst, Type src) =>
    dst == src ||                                              // T  dst <=> T src ... acceptable
      (dst.IsGenericType                                    && // T? dst <=> T src ... acceptable
       dst.GetGenericTypeDefinition() == typeof(Nullable<>) &&
       dst.GenericTypeArguments.FirstOrDefault() == src      );

  /// <summary>
  /// プロパティの値を置換する Action を作成する。(静的メソッド)
  /// (リフレクションを極小化するための式木)
  /// </summary>
  private static Action<object, object> CreatePropertyReplaceMethod(Type type_dst, Type type_src) {
    var props = type_src.GetProperties();

    // 式木構築前の型チェック (不都合がある場合は Runtime Exception)
    props
      .Select( p_src => type_dst.GetProperty(p_src.Name) switch {
        // 置換先のプロパティ存在チェック
        null      => throw new ArgumentException($"プロパティ {p_src.Name} は置換先インスタンスのクラス " +
                                                 $"{type_dst.Name} に存在しません。"),
        // 置換先のプロパティ型チェック
        var p_dst => (p_dst.PropertyType, p_src.PropertyType) switch {
          (var t_dst, var t_src) =>
            CheckPropertyType(t_dst, t_src) ?
            true : throw new ArgumentException($"プロパティ {p_src.Name} の型 {t_src.Name} が置換先インスタンスのクラス " +
                                               $"{type_dst.Name} のプロパティの型 {t_dst.Name} と一致しません。"),
        },
      } )
        .ToList();

    // Lambda Parameters
    var o_dst = Parameter(typeof(object));              // object dst (1st param of Action)
    var o_src = Parameter(typeof(object));              // object src (2nd param of Action)
    var t_dst = Parameter(type_dst);
    var t_src = Parameter(type_src);

    // Lambda Body
    var body  =
      (Enumerable.Empty<System.Linq.Expressions.Expression>()
       .Append(Assign(t_dst, Convert(o_dst, type_dst))) // T1 t_dst = (T1)o_dst;
       .Append(Assign(t_src, Convert(o_src, type_src))) // T2 t_src = (T2)o_src;
       .Concat(props
               .Select( x =>
                        Assign(Property(t_dst, x.Name), // t_dst.Prop[x] = (t_dst.Prop[x] の型)t_src.Prop[x];
                               Convert(Property(t_src, x.Name),
                                       type_dst.GetProperty(x.Name)?.PropertyType)) ))); // 不存在チェック済み

    // Compiling Lambda
    return
      (Action<object, object>)
        Lambda<Action<object, object>>(Block(new [] { t_dst, t_src }, body), o_dst, o_src)
          .Compile();
  }

  /// <summary>
  /// オブジェクトのクローンを Shallow Copy で作成する。
  /// (ICloneable 互換)
  /// </summary>
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
  public object Clone() => Clone(null);

  /// <summary>
  /// オブジェクトのクローンを Shallow Copy で作成する。
  /// 引数で指定される匿名クラスで同名プロパティの値を置き換える。
  /// </summary>
  public object Clone(object? param = null) =>
    new Func<object, object, object>((dst, src) => {
      var type_dst = dst.GetType();
      var type_src = src.GetType();
      var pattern  = $"{type_dst.Name}/{type_src.Name}";

      if (! DicAction.TryGetValue(pattern, out var action))
        DicAction.Add(pattern, action = CreatePropertyReplaceMethod(type_dst, type_src));

      action(dst, src);

      return dst;
    })(this.MemberwiseClone(),  // Clone shallow-copied
       param ?? new {}       ); // Substitute
}


クラスの辞書を静的に保持してキャッシュとし、同じクラスの組み合わせが来たらプロパティを置換するアクションをキャッシュから抽出して利用する。また、置換先 Record のプロパティの型が T? のときに置換元匿名クラスのプロパティをいちいち Nullable にキャストして指定するのが面倒であるため、Nullable か否かの違いは許容するようにしている。

  // もし、置換元と置換先のプロパティの型を完全一致させなければならない場合は、こう書かなければならない
  .Select( org => org.Clone(new { Value = (int?)1 }) as RecordSample )

  // 置換先のプロパティが int? であろうがこう書きたいため、T? <=> T の違いは許容するようにしている 
  .Select( org => org.Clone(new { Value = 1 }) as RecordSample )

  // 置換元のプロパティが null である場合は、キャストせざるを得ない (匿名型は型不定のプロパティを許容しない) 
  .Select( org => org.Clone(new { Value = (int?)null }) as RecordSample )


Record のプロパティ数 4、匿名クラスによる置換プロパティ数 1 の小さなデータクラスで試したところ、4 - 5 倍の高速化が図れた。めでたしめでたし。

繰り返し回数 100,000 1,000,000
旧版 (Reflection Only) 3,565ms 27,582ms
新版 (Expression) 692ms 6,695ms
高速化 5.2 倍 4.1 倍


実運用目的の式木を初めて自作してみて勉強になったが、コンパイル時、ランタイム初回実行時 (キャッシュがないとき)、ランタイム2回目以降実行時 (キャッシュヒットのとき) と 3 層構造で考えなければならず、この程度のロジック構築に半日もかかってしまった。

LINQ to Entities でロジック構築する場合も、LINQ to Object 互換レベル (IEnumerable)、LINQ to Entities 互換レベル (IQueryable)、データベースプロバイダー互換レベル (PostgreSQL では OK だが Oracle では NG というようなケース)、データベース実装互換レベル (Oracle 12c では OK だが Oracle 11g では NG な Apply 文など) と 4 層構造で考える必要がある。メタプログラミングは (おもしろいが) 非常に疲れる。

Training in Commercial Pilotage with FTD - Lead Design for Glide Path

都内某所で Flight Simulator による F/O 訓練。B737-800 で 1.5H。このお気に入り機種は、なんとなんと、まもなく退役で ... 詳細未公表のため詳しく書けないが、今後は B7x7-xxx (6 軸モーション!) と A3xx になるらしく、今回が私の B737-800 Last Flight に (╥_ ╥) アイチャクアル ヒコーキ トノ オワカレ サミシイヨ ... 機種移行訓練せねば ٩(ˊᗜ ˋ*)و メゲテル ヒマハ ナイ!

Acceleration / Deceleration Step Check

本日の訓練は Acceleration / Deceleration (増減速) の Step Check。

いつもの通り RJTT RW 34R から Take Off して 6,000 ft へ上がること 2 回。少し Power の絞り方が遅かったために 3 kt ほどオーバーしたりもするが、以前のようになかなか逸脱が収束しないということもなく、おおむね良い感じ。

今日は Acceleration / Deceleration がメインで、その練習をしてから Step Check に挑む予定だったのだが、教官の連携がうまくなく、240 → 280 kt の Acceleration を 1 回やったのみで N 教官へ引き継がれ Step Check へ。前回の反省を活かして Alt を維持する Pitch 調整を安定的にやったため、滑らかな A/S 移行ができたが、Acceleration より難しい Deceleration の練習をしないままの Step Check 突入であるため、当然、低速域では Lead 設計がうまくなく Deceleration の Exit は粗が目立つ。しかも最後は Stall 直前の 140 kt まで A/S を大幅に下げた Slow Flight をやることになったため、Pitch & Power 調整にかなり苦労する。それでも、レシプロ実機訓練で培った技量で、激しく逸脱することはなく収束させ、Step Check をクリアする。

この過程で発見したことだが、トレンドベクターは、Power を操作して A/S が動き始めたことを理由として動くのではなく、Pitch を傾けるだけでも感応する。A/S のトレンドベクターは A/S のみを情報源としているわけではなく、明らかに Pitch や Power の状況もすべて含めて複雑な「計算」をしている *1

後半の訓練は Constant Rate Climb / Descent。「500 ft/min で Climb」と教官に言われて、グイっと適当に Power を +10% くらい動かし、500 ft/min 前後を維持する。教官に「240 kt を維持して」とさらに要求され、「ん、Constant Rate で、かつ、Constant Speed で Climb って、N1 を何 % にするのか Power 設定を知っていなければできないでしょ」と心の中で反応しながら A/S に目をやると Climb 開始時に 242 kt くらいだった A/S が 244 kt → 245 kt と徐々に上がりつつある。教官がシミュレータを止め「なぜ Power を入れた?」と問う。 なんと答えたらよいかわからず「なぜって ... Climb だから ...」と漏らすと、「それは Power 水準で Alt がほぼ決まり、慣性を気にしなくてよいレシプロ機の操作方法だ」と返される。

ジェット機では「まず Pitch で姿勢を作って A/S ⇔ Alt (Climb / Descent の場合は VSI) の位置・運動エネルギー変換を行い、所望 Alt / VSI に設定したときに過不足する A/S を Power で補う」「先に Power 調整を行う *2 と調整の所要量がわからず A/S, Alt, VSI, が乱れて、整えるための操作をする必要に迫られるものの安定する水準を捉えられない結果、逸脱を収束させられずにワチャワチャすることになる」とのこと。さきほどの操作でいえば、Climb 開始時に 242 kt であったのであるから、まず、Pitch を上げて上昇し、240 kt に下がる間に 240 kt & 500 ft/min を維持できる Power を探る、ということになる。「Power は A/S 見合いで微調整するものであり、N1 = XX % という絶対水準を目指すものではない、だからこそ Power 出力水準を表示する EICAS はパイロットの視野中心から外れたところに配置されている」とも。

ということで、再度、Constant Speed & Rate Climb / Descent に挑戦すると ... なんだ、維持すべき基準点が Alt = 一定 すなわち VSI = 0 ft/min から VSI = 500 ft/min になるだけで、Straight & Level Flight とやることは変わらない! この訓練を始めたときは、こんな重い Yoke で Pitch 髪の毛 1 本のコントロールなんてムリ、まして、Constant Speed & Rate Climb/Descent なんて神業と思っていたが、自分にもできる! Climb / Decent Exit のリードは、レシプロのときと同じく、VSI = 500 ft/min なら 50 ft 前と VSI の 1/10 の数値 (= そのままレートを維持するとしたら 6 秒前相当) になる、また、今後 Traffic Pattern などをやっていく上で 750 ft/min が基本になるという。

最後にエンジン音を消すと、途端に操縦がガタガタになり、やってはならない VSI 追従をしてしまう。いかに普段、視覚以外の情報に頼っていたかがわかる試行だった。次回以降は 6 軸モーションを使うこともできるため、新しい発見があるかもしれない。

次回以降に備えて、降下角 3° (Final Approach)、4° (TOD からの Descent)、3.5° (いま話題の RJTT RW 16R/16L Approach) の Descent Rate と A/S の関係を計算しておく *3

f:id:Crayon:20190316120104j:plain:w480:right

Air Speed (kt) Descent Angle (degree)
3.0 3.5 4.0
Descent Rate
(ft/min)
500 110 95 83
750 165 142 124
1,000 221 189 165
1,250 276 236 207
1,500 331 284 248
1,750 386 331 289
2,000 441 378 331

本日の重要ポイント

  • Constant Speed & Rate Climb の操作方法
    • まず Pitch で姿勢を作って A/S ⇔ Alt (Climb / Descent の場合は VSI) の位置・運動エネルギー変換を行う
    • 位置・運動エネルギーの過不足は A/S に現れるため、A/S の過不足の量をみて Power を調整する
    • 基準点を所定 VSI に置くだけであり、Straight & Level の操作と本質は変わらない
  • Exit Lead は VSI の 1/10 の数値 (= 6 秒前相当)
  • Traffic Pattern では 750 ft/min が基準となる

*1:トレンドベクターは 10 秒後の状況を予測しているとのこと。

*2:ジェット機でも Power = 63%, Alt =6,000 ft と固定目標を定めておけば、理論上は A/S = 240 ft へ徐々に収束することになるが、Pitch が揺らげばそれだけ Drug となり、Gust 等でも諸元を狂わされるため、実際には収束しないという。

*3:こうして計算してみると Final Approach の Glide Path を 3° → 3.5° にすると、Descent Rate を維持するなら A/S がかなり減じられる = 迎角が大きくなってパイロットの視線と滑走路が成す角度は 0.5° 以上に大きくなり、また Stall すれすれの低速になるため安全バッファーがなくなる、一方で A/S を 165 kt に維持するなら Descent Rate を 2 割増の 900 ft/min 程度にしなければならず、これまた判断や操作のリードタイムが削られ安全バッファーがなくなることがよくわかる。

Functional Programming w/ C# LINQ

C# 8 switch 式の威力はすごい。EntityFramework Core 用の Upsert が実質 3 行 *1 で記述できてしまった *2

public static void Upsert<T>(this DbSet target, IEnumerable<T> source) where T : class, IDbMappable =>
  source
    .GroupJoin(target                    ,
               l      => l.GetKeyObject(),
               r      => r.GetKeyObject(),
               (l, r) => r.FirstOrDefault() switch { // data-driven table operation with side-effect expected
                 null                     => target.Add(l) as T, // Insert
                 var f when ! f.Equals(l) => f.Substitute(l)   , // Update
                 _                        => l                 , // Nop
               })
       .ToList();

同一キーレコードであるものないもの、同一値レコードであるものを GroupJoin() と switch 式を用いることでグループ分けし、データドリブンで次のアクションを Insert / Update / Nop (何もしない) と振り分ける。

ラムダ式を要求する LINQ によるデータ処理であるため switch 式を使うのだが、switch 文 ではなくわざわざ switch「式」を使っているにも関わらず、副作用に依拠してテーブル処理しており、逆に出力 ToList() は何も利用していないのがトリッキーである。switch 式がすばらしいのは、変数宣言式が使えない現行 C# にあって、var f として型推論・変数代入で受けられるところ。これにより手続や条件分岐を冗長に記述することなく Equals ()や Substitute() *3 といった式にかけられる。

LINQ を使っていると、データ変換の一連の式の内部で途中結果を複数回参照する場面が多々あり、

var obj    = new { Text = "Sample", Value = 1 };
var result = (var x = obj.Value + 1) * x;

のように、式の流れを止めずに変数宣言式を用いて中間変数を定義したいところだが、これができない。今後は、代わりに

var result = (obj.Value + 1) switch { var x => x * x };

と中間変数を受けるだけの目的で switch 式を使うようになるかもしれない。

*1:Upsert の本質は (l, r) ~ f.Substitute(l) の 3 行。

*2:IDbMappable はデータベースにマップする Record が具備すべきインターフェースとして定義しており、主キーとなるオブジェクトを返す GetKeyObject() メソッドをデフォルト実装している。

*3:Substitute() は Record の全プロパティ値を引数のそれに置き換えるメソッドとして定義している。= で代入すると変数の中身ではなく参照が置換されてしまうため。

Training in Commercial Pilotage with FTD - Scanning

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

Acceleration / Deceleration

本日の訓練内容は Acceleration / Deceleration (増減速)。前回と同じ K 教官。

前回 Take Off からの Climb & Level Off がうまくいったため、今回もある程度うまくいくかと思いきや、Heading 100 へ右旋回を指定されていたため転針に気を取られ、教官に指摘されるまで上昇を続けてしまい、700 ft も Overshoot してしまう。前回と比較するといろいろとブレるものの、それでも以前よりも Scanning と操作が丁寧になっており、大きく逸脱することはない。ということで増減速の練習開始。

10 回弱の増減速訓練と教官の助言により開眼。これまで一切できていなかった操作のリード設計ができるようになり、また、操作と Scanning が連動するようになる。操縦とは (Pitch & Roll の) 逸脱を収束させることと悟りけり。実感として得たポイントは下記のとおり。

本日の重要ポイント

Pitch の揺らぎと Roll (Bank) の揺らぎを同時に制する

  • 全般
    • Pitch も Bank も Power も (所望方向へ動くために) 変化させたら (安定する水準へ) 戻す
    • Yoke はきちんとグリップし、Pitch Up / Down ともに反応が遅れないようにする*1
    • Yoke 操作する腕はアームレストを使い、Pitch を固定できるようにする
  • Altitude
    • VSI の揺らぎを "先読み" してフゴイドを収束させる
    • ただし、操作の基準水準は Pitch に置く (VSI を基準にすると Delayed Feedback により揺らぎが発散する)
    • 増減速では Power 操作後、徐々に変化していく VSI 揺らぎの強さと水準に応じて Pitch の基準水準を変えていき、Altitude を維持する
    • 高速時は Pitch に対する Altitude の感応度が高いため、Pitch は特に精緻に制御する
    • 低速時, 減速/降下 Exit 時は Power に対する Altitude の感応度が高いため、Power 操作は適切なリードを設計する
  • A/S
    • Altitude を維持していれば、徐々に意図した方向へ増減速していく (Altitude を維持しないと A/S が意図した方向へ安定的に移行しない)
    • Trend Vector (A/S 加速度 を示す Green Arrow) が Target A/S の Bug を超えないように徐々に Power を修正して Exit を図る
  • Power
    • 6,000 ft なら増減速 Exit 時に安定する Power Settings は 220 kts = 61%, 240 kts = 63%, 280 kts = 67% くらい (現在は訓練のためいつも重量固定としている *2 )
    • 適切なリード, タイミング, 変量を設計する (早くても遅くても Altitude か A/S が大きく狂う)
    • 急操作はしない (急操作をするとその後の変化にうまく対応できなくなる確率が高い)
    • EICAS は凝視しない、しかし、Power 操作前の水準、操作後の水準と左右出力差は必ず確認する
    • 左席から右手で Throttle 操作をすると小指側 (右エンジン) の出力が相対的に低くなる傾向がある
  • Bank
    • PFD 上部を見て Bank Indicator の揺らぎを収束させる
    • ND は結果としての Heading Deviation がわかるため、これを情報源とすると事後修正しかできない
    • Bank が 0 であれば Heading はズレない
    • Bank 0 ±0.2° (1 ドット) 以内で収束させる精度が身につけば
      • Bank/Heading 調整は PFD 上部/下部の Bank Indicator/Heading Indicator で完結する
      • エンジン左右の出力差が Heading に与える影響を感じられるようになり、N1 Power Balance を繊細に制御するようになる

*1:これまで Up Trim ぎみにセットし Yoke を常に押すことで Neutral としておき、圧を緩めることで Pitch Up していたが、その方法だと Up / Down が非対称となる。特に Trim が Fit する点以上に Yoke を引かなければならない時に圧緩和から Yoke を握り直して人差し指で引く操作へと必要動作が変わってしまうため、スムーズな移行と繊細な操作ができておらず Pitch Up の反応が遅れる傾向にあった、と思う。

*2:本来、諸元は高度と重量に依存する。