JavaScript で実装して総復習する
音速、吸収、伝搬損失、往復時間、配列利得、信号対雑音比の式を JavaScript の関数に対応させ、演習で全体を振り返ります。
この章で行うこと
これまでに学んだ式を、JavaScript の小さな関数として実装します。数式の記号が、関数名、引数、戻り値のどこに対応するかを確かめながら読み進めましょう。
前半ではコードを読み、後半の演習では、距離、周波数、受動・能動の違い、素子数の関係をまとめて振り返ります。
式と関数の対応を先に見る
| 求める値 | 式または考え方 | JavaScript の関数 |
|---|---|---|
| 音速 c | 水温、塩分、深さを使う Mackenzie の近似式 | mackenzieSoundSpeedMps |
| 吸収係数 α | 周波数だけを使う教材用の簡略式 | absorptionDbPerKm |
| 波長 λ | λ = c / f | wavelengthM |
| 片道の伝搬損失 TL | k log10(R) + αR / 1000 | transmissionLossDb |
| 音の往復時間 | 2R / c | roundTripTimeSec |
| 指向性指数 DI | 10 log10(N) の理想近似 | arrayGainDb |
| 受動 SNR | SL - TL - (NL - DI) | passiveSnrDb |
| 能動 SNR | SL - 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 つのまとまりに分けて読む
- 入力の確認:
assertFiniteNumber、assertRange、assertPositiveIntegerが、不正な値を計算の前に止めます。 - 海中の条件から基礎量を求める:
mackenzieSoundSpeedMpsが音速を、absorptionDbPerKmが 1 km 当たりの吸収を返します。 - 距離や配列に関する量を求める:
wavelengthM、transmissionLossDb、roundTripTimeSec、arrayGainDbが、それぞれ対応する式をそのまま計算します。 - 最後に SNR を求める:受動では片道の
TLを 1 回、能動では往復分の2TLを差し引きます。
すべてを一度に理解しようとせず、入力から戻り値までを一つの関数ずつ追ってください。特に transmissionLossDb の戻り値が、2 つの SNR 関数へどう渡るかを見ると全体がつながります。
演習で全体をつなげよう
ここからは、関数の戻り値を求める問題と、ソナーの使い方を考える問題に取り組みます。必要ならコードをブラウザの開発者ツールや Node.js で動かし、計算結果を確かめてください。
距離を伸ばすと TL が増えること、周波数を上げると波長が短くなる一方で吸収が増えること、素子数を増やすと DI が大きくなることを、式と関数の両方から説明できれば総復習は完了です。
実装とソナーの基礎を総復習しよう
関数が返す値を計算し、距離、方式、素子数の関係を 7 問で確認します。
Q1. roundTripTimeSec(rangeM, soundSpeedMps) に (1500, 1500) を渡すと、何秒が返りますか。
往復時間は 2 × range / c で求めます。
2 × 1500 / 1500 = 2 なので、2.0 s が返ります。
Q2. wavelengthM(1500, 30000) を実行すると、何 m が返りますか。
波長は 1500 / 30000 で求めます。
1500 / 30000 = 0.05 なので、0.05 m が返ります。
Q3. この実装に負の距離や範囲外の水温を渡すと、どの種類の例外が発生しますか。
値が許される範囲から外れたときに使う例外です。
入力値が前提の範囲から外れたことを明確に示すため、RangeError を投げます。
Q4. arrayGainDb(10) を実行すると、およそ何 dB が返りますか。
10 log10(10) です。
10 log10(10) = 10 なので、約 10 dB です。
Q5. 吸収を無視し、球面拡散だけで考えます。距離を半分にすると、片道の TL は約 6 dB 小さくなります。このとき、能動ソナーのエコー SNR はおよそ何 dB 改善しますか。
能動ソナーの式には、往復分の 2TL が入ります。
片道の TL が 6 dB 小さくなると、往復ではその 2 倍が効くため、能動 SNR は約 12 dB 改善します。この問題は吸収を無視した考察です。実装の transmissionLossDb は吸収も含めて計算するため、実際の戻り値は周波数によって変わります。
Q6. 同じ環境で距離を伸ばしたとき、基本のソナー方程式では、受動ソナーと能動ソナーのどちらがより不利になりやすいですか。
能動ソナーでは、音が目標まで往復します。
能動ソナーの式には 2TL が入るため、距離が伸びたときの損失は受動ソナーより大きくなります。
Q7. クジラの鳴き声や船舶の騒音を、こちらから音を出さずに監視したい場合、まず選ぶべき方式はどれですか。
自分から音を出さず、周囲の音を聴く方式を選びます。
受動ハイドロフォンアレイは、自分から音を送信せずに海中の音を受信できるため、この用途に適しています。
この講座の着地点
- 数式の入力と出力を、そのまま JavaScript の引数と戻り値に対応させると、実装を追いやすくなります。
- 入力が前提の範囲を外れたときは、
RangeErrorで明確に止めます。 - 音速、伝搬損失、配列利得、受動・能動の SNR を、別々の関数として組み合わせられます。
- 実際の設計では、屈折、残響、海面・海底での散乱、検出しきい値なども追加します。