合同会社小村ソフト
第 7 章

最小実装と総合演習 — vanilla JavaScript でソナー計算を書く

Mackenzie 音速・TL・配列利得・SNR を vanilla JavaScript で短く書き、ケースで全体を再構成する。

第 I 部: 最小実装を読む

本章は 2 部構成です。第 I 部(本節〜「コードと式の対応」表)では、第 2〜5 章で扱った式を vanilla JavaScript の関数に対応付けて読んでいきます。続く第 II 部で、それらの関数を組み合わせるケース問題に進みます。

最小実装の方針

この講座の実装は vanilla JavaScript だけで完結させています。外部ライブラリは使わず、前提を満たさない入力は RangeError で止めます。入門教材では、暗黙に丸めたりクランプしたりするより、前提違反をはっきり可視化した方が式とコードの対応を追いやすいからです。

ブラウザ互換性に関する注意: 以下のコードでは Math.log10Number.isFiniteNumber.isInteger** 演算子(指数演算子)など ECMAScript 2015 (ES6) 以降で導入された機能を使用しています。最近のブラウザ(Chrome 44+、Firefox 25+、Safari 10+、Edge 12+)では問題なく動作しますが、Internet Explorer など極めて古い環境では動作しません。レガシー環境向けにはポリフィル(例: Math.log10 = Math.log10 || function (x) { return Math.log(x) / Math.LN10; };)の追加を検討してください。

最小実装の JavaScript

function assertFiniteNumber(name, value) {
  if (!Number.isFinite(value)) {
    throw new RangeError(`${name} must be a finite number`);
  }
}

function assertRange(name, value, min, max) {
  assertFiniteNumber(name, value);
  if (value < min || value > max) {
    throw new RangeError(`${name} must be in [${min}, ${max}]`);
  }
}

function assertPositiveInteger(name, value) {
  if (!Number.isInteger(value) || value <= 0) {
    throw new RangeError(`${name} must be a positive integer`);
  }
}

function mackenzieSoundSpeedMps(tempC, salinityPsu, depthM) {
  assertRange('tempC', tempC, 2, 30);
  assertRange('salinityPsu', salinityPsu, 25, 40);
  assertRange('depthM', depthM, 0, 8000);
  return 1448.96
    + 4.591 * tempC
    - 5.304e-2 * tempC ** 2
    + 2.374e-4 * tempC ** 3
    + 1.340 * (salinityPsu - 35)
    + 1.630e-2 * depthM
    + 1.675e-7 * depthM ** 2
    - 1.025e-2 * tempC * (salinityPsu - 35)
    - 7.139e-13 * tempC * depthM ** 3;
}

// Educational simplified absorption formula (frequency only).
// Derived in form from François-Garrison (1982) / Ainslie-McColm (1998),
// but with temperature, salinity, pH and depth dependence removed.
// For field design, use the full models with environmental inputs.
function absorptionDbPerKm(frequencyKhz) {
  assertRange('frequencyKhz', frequencyKhz, 0.4, 200);
  const f2 = frequencyKhz ** 2;
  return 0.11 * f2 / (1 + f2)
    + 44 * f2 / (4100 + f2)
    + 0.000275 * f2
    + 0.003;
}

function wavelengthM(soundSpeedMps, frequencyHz) {
  assertRange('soundSpeedMps', soundSpeedMps, 1300, 1700);
  assertRange('frequencyHz', frequencyHz, 1, 1_000_000);
  return soundSpeedMps / frequencyHz;
}

function transmissionLossDb(rangeM, frequencyKhz, spreadingCoeff = 20) {
  assertRange('rangeM', rangeM, 1, 1_000_000);
  assertRange('spreadingCoeff', spreadingCoeff, 10, 20);
  return spreadingCoeff * Math.log10(rangeM)
    + absorptionDbPerKm(frequencyKhz) * (rangeM / 1000);
}

// roundTripTimeSec: returns 2R / c.
// Lower bound on rangeM is 1 m because a zero range is physically meaningless
// for a sonar problem (target would coincide with the transducer).
function roundTripTimeSec(rangeM, soundSpeedMps) {
  assertRange('rangeM', rangeM, 1, 1_000_000);
  assertRange('soundSpeedMps', soundSpeedMps, 1300, 1700);
  return (2 * rangeM) / soundSpeedMps;
}

function arrayGainDb(elementCount) {
  assertPositiveInteger('elementCount', elementCount);
  return 10 * Math.log10(elementCount);
}

function passiveSnrDb(sourceLevelDb, tlDb, noiseLevelDb, diDb) {
  return sourceLevelDb - tlDb - (noiseLevelDb - diDb);
}

function activeSnrDb(sourceLevelDb, tlDb, targetStrengthDb, noiseLevelDb, diDb) {
  return sourceLevelDb - 2 * tlDb + targetStrengthDb - (noiseLevelDb - diDb);
}

この実装は、距離・波長・TL・SNR を教材の中で一貫して計算するための最小集合です。実海域の設計では境界損失、残響、屈折、伝搬モデル、帯域、検出閾値などを追加しますが、入門としてはここまでで十分です。

コードと式の対応

関数意味
mackenzieSoundSpeedMps温度・塩分・深さから音速 c を近似する
wavelengthMλ = c / f
transmissionLossDb拡散 + 吸収で片道 TL を近似する
roundTripTimeSec2R / c
arrayGainDb10 log10(N) の理想近似
passiveSnrDb受動 SNR の簡略式
activeSnrDb能動 SNR の簡略式

最後の総合演習では、距離を伸ばすと active の SNR が急速に低下すること、高周波は短波長だが吸収が増えること、素子数増加が DI を通じて効くことを、ケース問題として再構成します。

第 II 部: ケースで全体を再構成する

ここから先は 総合演習 パートです。前半(第 I 部)で読んだ最小実装の各関数を頭の中で組み合わせ、距離・周波数・方式・素子数のトレードオフを 1 つのストーリーとして再構成してください。コードを実際に動かしながら答えを出すと、関数同士の依存関係も自然に整理されます。

この章の理解チェック — 最小実装と総合演習

関数の戻り値を読む問題と、距離・方式・素子数のトレードオフをつなぐケース問題を 7 問確認します。

Q1. 実装章の関数 roundTripTimeSec(rangeM, soundSpeedMps)(1500, 1500) を渡したとき、戻り値は何秒ですか。

2 × range / c です。

s

Q2. wavelengthM(1500, 30000) の戻り値は何 m ですか。

1500 / 30000 です。

m

Q3. この講座の最小実装では、負の距離や範囲外の温度のような前提違反が来たとき、どの例外で明示的に止める方針にしていますか。

値の範囲が前提から外れたときの例外です。

Q4. arrayGainDb(10) はおよそ何 dB を返しますか。

10 log10(10) です。

dB

Q5. 概念整理のため、球面拡散のみを考え、吸収は無視する近似で考えます(実際の transmissionLossDb は常に吸収を含めて計算するため、本問は単純化された理論問題です)。距離を半分にすると 1-way の TL は約 6 dB 改善します。では、能動 echo SNR はおおよそ何 dB 改善するイメージですか。

active は 2TL です。

Q6. 同じ環境・同じ 1-way TL で比較したとき、単純な基礎式では距離が伸びるほど不利になりやすいのはどちらですか。

active は out-and-back です。

Q7. クジラや船舶騒音の存在監視を、できるだけ送信なしで行いたい場面を考えます。入門としてまず選びやすい方式はどれですか。

listen only がキーワードです。

この講座の着地点

  • 最小実装では、数式をそのまま関数へ写し、前提違反は RangeError で止める。
  • Mackenzie 音速近似、TL 近似、SNR 近似、配列利得近似の 4 本柱を押さえる。
  • 最後は距離・周波数・方式・素子数のトレードオフをケースで結び直す。
  • 現場設計では、教材の外側にある屈折・残響・境界散乱・検出閾値を追加する。