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

JavaScript で実装して総復習する

音速、吸収、伝搬損失、往復時間、配列利得、信号対雑音比の式を JavaScript の関数に対応させ、演習で全体を振り返ります。

この章で行うこと

これまでに学んだ式を、JavaScript の小さな関数として実装します。数式の記号が、関数名、引数、戻り値のどこに対応するかを確かめながら読み進めましょう。

前半ではコードを読み、後半の演習では、距離、周波数、受動・能動の違い、素子数の関係をまとめて振り返ります。

式と関数の対応を先に見る

求める値式または考え方JavaScript の関数
音速 c水温、塩分、深さを使う Mackenzie の近似式mackenzieSoundSpeedMps
吸収係数 α周波数だけを使う教材用の簡略式absorptionDbPerKm
波長 λλ = c / fwavelengthM
片道の伝搬損失 TLk log10(R) + αR / 1000transmissionLossDb
音の往復時間2R / croundTripTimeSec
指向性指数 DI10 log10(N) の理想近似arrayGainDb
受動 SNRSL - TL - (NL - DI)passiveSnrDb
能動 SNRSL - 2TL + TS - (NL - DI)activeSnrDb

たとえば wavelengthM は、音速を周波数で割って波長を返します。関数名と式を一組ずつ結び付けると、長いコードも読みやすくなります。

入力値を先に確かめる

実装は外部ライブラリを使わない、標準的な JavaScript です。計算の前に、値が有限か、決めた範囲に入っているか、素子数が正の整数かを確認します。

条件に合わない値は、勝手に丸めたり上限に合わせたりせず、RangeError で処理を止めます。こうすると、どの入力が式の前提を外れたのかをすぐに確認できます。コードは現在の主要なブラウザや Node.js でそのまま実行できます。

最小実装の 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);
}

このコードは、講座で使った式の関係を確認するための最小構成です。実際の海域を扱うときは、海面・海底での損失、残響、屈折、帯域幅、検出しきい値なども加えます。

コードは 4 つのまとまりに分けて読む

  1. 入力の確認assertFiniteNumberassertRangeassertPositiveInteger が、不正な値を計算の前に止めます。
  2. 海中の条件から基礎量を求めるmackenzieSoundSpeedMps が音速を、absorptionDbPerKm が 1 km 当たりの吸収を返します。
  3. 距離や配列に関する量を求めるwavelengthMtransmissionLossDbroundTripTimeSecarrayGainDb が、それぞれ対応する式をそのまま計算します。
  4. 最後に SNR を求める:受動では片道の TL を 1 回、能動では往復分の 2TL を差し引きます。

すべてを一度に理解しようとせず、入力から戻り値までを一つの関数ずつ追ってください。特に transmissionLossDb の戻り値が、2 つの SNR 関数へどう渡るかを見ると全体がつながります。

演習で全体をつなげよう

ここからは、関数の戻り値を求める問題と、ソナーの使い方を考える問題に取り組みます。必要ならコードをブラウザの開発者ツールや Node.js で動かし、計算結果を確かめてください。

距離を伸ばすと TL が増えること、周波数を上げると波長が短くなる一方で吸収が増えること、素子数を増やすと DI が大きくなることを、式と関数の両方から説明できれば総復習は完了です。

実装とソナーの基礎を総復習しよう

関数が返す値を計算し、距離、方式、素子数の関係を 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. 吸収を無視し、球面拡散だけで考えます。距離を半分にすると、片道の TL は約 6 dB 小さくなります。このとき、能動ソナーのエコー SNR はおよそ何 dB 改善しますか。

能動ソナーの式には、往復分の 2TL が入ります。

Q6. 同じ環境で距離を伸ばしたとき、基本のソナー方程式では、受動ソナーと能動ソナーのどちらがより不利になりやすいですか。

能動ソナーでは、音が目標まで往復します。

Q7. クジラの鳴き声や船舶の騒音を、こちらから音を出さずに監視したい場合、まず選ぶべき方式はどれですか。

自分から音を出さず、周囲の音を聴く方式を選びます。

この講座の着地点

  • 数式の入力と出力を、そのまま JavaScript の引数と戻り値に対応させると、実装を追いやすくなります。
  • 入力が前提の範囲を外れたときは、RangeError で明確に止めます。
  • 音速、伝搬損失、配列利得、受動・能動の SNR を、別々の関数として組み合わせられます。
  • 実際の設計では、屈折、残響、海面・海底での散乱、検出しきい値なども追加します。