import get from 'lodash-es/get.js'
import size from 'lodash-es/size.js'
import each from 'lodash-es/each.js'
import range from 'lodash-es/range.js'
import take from 'lodash-es/take.js'
// import reverse from 'lodash-es/reverse.js' //僅舊版hzs對稱建構使用, 已改為min(k,n-k)遮罩
import ispnum from 'wsemi/src/ispnum.mjs'
import isp0num from 'wsemi/src/isp0num.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import { fft, ifft, complex } from 'mathjs'
//舊版: 取得>=n的最小2冪次, 供ml-fft補零至2冪次用; 改用mathjs後不需要, 保留供比對
/*
function get2n(n) {
let i = 1
let j = Math.pow(2, 52)
while (true) {
i *= 2
// console.log('n', n, 'i', i, 'n <= i', n <= i)
if (n <= i) {
break
}
// console.log('j', j, 'i >= j', i >= j)
if (i >= j) {
break
}
}
return i
}
*/
//舊版: 基於ml-fft, 先用get2n將n補零至2冪次(nCols)再做FFT; 非2冪次時實際為nCols點DFT(對DTFT的插值), 與真實n點DFT(MATLAB fft)不同, 保留供比對
/*
function _fft1d(arr, mode = 'norm') {
//check
if (mode !== 'norm' && mode !== 'inv') {
throw new Error(`invalid mode[${mode}]`)
}
//n
let n = size(arr)
// console.log('n', n)
//nCols
let nCols = get2n(n)
// console.log('nCols', nCols)
//init
FFT.init(nCols)
//fill
let re = new Array(nCols)
let im = new Array(nCols)
if (mode === 'norm') {
for (let i = 0; i < nCols; i++) {
let _i = get(arr, i, 0) //超過n至nCols之元素自動補0
let _j = 0
// console.log('_i', _i, '_j', _j)
re[i] = _i
im[i] = _j
}
}
else {
for (let i = 0; i < nCols; i++) {
let _i = get(arr, `${i}.0`, 0)
let _j = get(arr, `${i}.1`, 0)
// console.log('_i', _i, '_j', _j)
re[i] = _i
im[i] = _j
}
}
let res = []
if (mode === 'norm') {
FFT.fft(re, im)
for (let i = 0; i < nCols; i++) {
let _i = re[i]
let _j = im[i]
res.push([_i, _j])
}
}
else {
FFT.ifft(re, im)
for (let i = 0; i < nCols; i++) {
let _i = re[i]
// let _j = im[i]
// let _l = Math.sqrt(_i * _i, _j * _j)
res.push(_i)
}
}
return res
}
*/
//新版: 基於mathjs, 直接對任意n點做真實n點DFT(對齊MATLAB fft), 不再補零至2冪次
function _fft1d(arr, mode = 'norm') {
//check
if (mode !== 'norm' && mode !== 'inv') {
throw new Error(`invalid mode[${mode}]`)
}
//n
let n = size(arr)
// console.log('n', n)
//fft, mathjs對任意n: 2冪次走Cooley-Tukey, 其餘走Chirp-Z(CZT), 皆為真實n點DFT
let res = []
if (mode === 'norm') {
//fill, 實數輸入(虛部視為0)
let cs = new Array(n)
for (let i = 0; i < n; i++) {
cs[i] = get(arr, i, 0)
}
//fft, 回傳mathjs Complex物件陣列(含re, im)
let out = fft(cs)
//res
for (let i = 0; i < n; i++) {
res.push([out[i].re, out[i].im])
}
}
else {
//fill, 複數輸入[[re,im],...]
let cs = new Array(n)
for (let i = 0; i < n; i++) {
let _i = get(arr, `${i}.0`, 0)
let _j = get(arr, `${i}.1`, 0)
cs[i] = complex(_i, _j)
}
//ifft, mathjs之ifft已正規化(內部除以n), round-trip可還原原訊號
let out = ifft(cs)
//res, 只取實部(與舊版一致)
for (let i = 0; i < n; i++) {
res.push(out[i].re)
}
}
return res
}
/**
* FFT1D
*
* @param {Array} arr 輸入數據陣列
* @return {Array} 回傳轉換後數據陣列
* @example
*
* let arr
* let res
*
* arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
* res = wf.fft1d(arr)
* console.log(res)
* // => [
* // [ 120, 0 ],
* // [ -8, 40.21871593700678 ],
* // [ -8, 19.31370849898476 ],
* // [ -8.000000000000002, 11.972846101323912 ],
* // [ -8, 8 ],
* // [ -7.999999999999999, 5.345429103354391 ],
* // [ -8, 3.3137084989847594 ],
* // [ -8.000000000000002, 1.5912989390372623 ],
* // [ -8, 0 ],
* // [ -8, -1.5912989390372623 ],
* // [ -7.999999999999999, -3.3137084989847594 ],
* // [ -7.999999999999998, -5.345429103354393 ],
* // [ -7.999999999999999, -8 ],
* // [ -7.999999999999999, -11.97284610132391 ],
* // [ -7.999999999999997, -19.31370849898476 ],
* // [ -7.999999999999993, -40.21871593700678 ]
* // ]
*
*/
let fft1d = (arr) => {
return _fft1d(arr, 'norm')
}
/**
* iFFT1D
*
* @param {Array} arr 輸入數據陣列
* @return {Array} 回傳轉換後數據陣列
* @example
*
* let arr
* let res
*
* arr = [
* [120, 0],
* [-8, 40.21871593700678],
* [-8, 19.31370849898476],
* [-7.999999999999999, 11.972846101323913],
* [-8, 8],
* [-8, 5.345429103354391],
* [-8, 3.313708498984761],
* [-7.999999999999999, 1.5912989390372623],
* [-8, 0],
* [-7.999999999999999, -1.5912989390372623],
* [-8, -3.313708498984761],
* [-8, -5.345429103354391],
* [-8, -8],
* [-7.999999999999999, -11.972846101323913],
* [-8, -19.31370849898476],
* [-8, -40.21871593700678]
* ]
* res = wf.ifft1d(arr)
* console.log(res)
* // => [
* // 0, 1,
* // 2.0000000000000004, 3.0000000000000013,
* // 4, 5.000000000000002,
* // 6, 7.000000000000001,
* // 8, 9,
* // 10, 10.999999999999998,
* // 12, 12.999999999999998,
* // 14, 15
* // ]
*
*/
let ifft1d = (arr) => {
return _fft1d(arr, 'inv')
}
// function __fft2d(re, im) {
// let tre = []
// let tim = []
// let i = 0
// // x-axis
// for (let y = 0; y < _n; y++) {
// i = y * _n
// for (let x1 = 0; x1 < _n; x1++) {
// tre[x1] = re[x1 + i]
// tim[x1] = im[x1 + i]
// }
// fft1d(tre, tim)
// for (let x2 = 0; x2 < _n; x2++) {
// re[x2 + i] = tre[x2]
// im[x2 + i] = tim[x2]
// }
// }
// // y-axis
// for (let x = 0; x < _n; x++) {
// for (let y1 = 0; y1 < _n; y1++) {
// i = x + y1 * _n
// tre[y1] = re[i]
// tim[y1] = im[i]
// }
// fft1d(tre, tim)
// for (let y2 = 0; y2 < _n; y2++) {
// i = x + y2 * _n
// re[i] = tre[y2]
// im[i] = tim[y2]
// }
// }
// }
// function __ifft2d(re, im) {
// let tre = []
// let tim = []
// let i = 0
// // x-axis
// for (let y = 0; y < _n; y++) {
// i = y * _n
// for (let x1 = 0; x1 < _n; x1++) {
// tre[x1] = re[x1 + i]
// tim[x1] = im[x1 + i]
// }
// ifft1d(tre, tim)
// for (let x2 = 0; x2 < _n; x2++) {
// re[x2 + i] = tre[x2]
// im[x2 + i] = tim[x2]
// }
// }
// // y-axis
// for (let x = 0; x < _n; x++) {
// for (let y1 = 0; y1 < _n; y1++) {
// i = x + y1 * _n
// tre[y1] = re[i]
// tim[y1] = im[i]
// }
// ifft1d(tre, tim)
// for (let y2 = 0; y2 < _n; y2++) {
// i = x + y2 * _n
// re[i] = tre[y2]
// im[i] = tim[y2]
// }
// }
// }
// function fft2d(mat, mode = 'norm') {
// //check
// if (mode !== 'norm' && mode !== 'inv') {
// throw new Error(`invalid mode[${mode}]`)
// }
// //m, n
// let m = size(mat)
// let n = size(get(mat, 0, []))
// console.log('m', m)
// console.log('n', n)
// //nRows, nCols
// let nRows = get2n(m)
// let nCols = get2n(n)
// console.log('nRows', nRows)
// console.log('nCols', nCols)
// //data
// let data = new Array(nRows * nCols)
// for (let i = 0; i < nRows; i++) {
// for (let j = 0; j < nCols; j++) {
// data[i * nCols + j] = get(mat, `${i}.${j}`, 0) //超過元素自動補0
// }
// }
// console.log('data', data)
// let res = []
// if (mode === 'norm') {
// let ftData = FFTUtils.fft2DArray(data, nRows, nCols)
// console.log('ftData', ftData, size(ftData))
// }
// else {
// }
// // let ftData = FFTUtils.fft2DArray(data, nRows, nCols)
// // let ftRows = nRows * 2
// // let ftCols = nCols / 2 + 1
// // let iftData = FFTUtils.ifft2DArray(ftData, ftRows, ftCols)
// }
//基於mathjs, 對任意m×n矩陣做真實2D DFT(對齊MATLAB fft2); mathjs內部對每列每行各做1D(可分離), 各維皆支援任意長度(2冪次走Cooley-Tukey, 其餘走Chirp-Z)
function _fft2d(mat, mode = 'norm') {
//check
if (mode !== 'norm' && mode !== 'inv') {
throw new Error(`invalid mode[${mode}]`)
}
//m, n (列數, 行數); n以第0列長度為準, 缺項補0
let m = size(mat)
let n = size(get(mat, 0, []))
// console.log('m', m, 'n', n)
//fft 2D
let res = []
if (mode === 'norm') {
//fill, 實數輸入矩陣(虛部視為0)
let cs = []
for (let i = 0; i < m; i++) {
let row = new Array(n)
for (let j = 0; j < n; j++) {
row[j] = get(mat, `${i}.${j}`, 0)
}
cs.push(row)
}
//fft, 回傳mathjs Complex物件之巢狀陣列
let out = fft(cs)
//res, 每元素轉[re,im]
for (let i = 0; i < m; i++) {
let row = []
for (let j = 0; j < n; j++) {
row.push([out[i][j].re, out[i][j].im])
}
res.push(row)
}
}
else {
//fill, 複數輸入矩陣[[[re,im],...],...]
let cs = []
for (let i = 0; i < m; i++) {
let row = new Array(n)
for (let j = 0; j < n; j++) {
let _i = get(mat, `${i}.${j}.0`, 0)
let _j = get(mat, `${i}.${j}.1`, 0)
row[j] = complex(_i, _j)
}
cs.push(row)
}
//ifft, mathjs之ifft已正規化(內部除以m*n), round-trip可還原原矩陣
let out = ifft(cs)
//res, 只取實部(與1D一致)
for (let i = 0; i < m; i++) {
let row = []
for (let j = 0; j < n; j++) {
row.push(out[i][j].re)
}
res.push(row)
}
}
return res
}
/**
* FFT2D
*
* @param {Array} mat 輸入二維實數矩陣
* @return {Array} 回傳二維[re,im]複數矩陣
* @example
*
* let mat
* let res
*
* mat = [[1, 2, 3], [4, 5, 6]]
* res = wf.fft2d(mat)
* console.log(res)
* // => [
* // [ [ 21, 1.0445074572148558e-16 ], [ -2.9999999999999982, 1.7320508075688785 ], [ -3.0000000000000013, -1.7320508075688705 ] ],
* // [ [ -9, -2.326366143623307e-16 ], [ 0, -8.881784197001252e-16 ], [ 0, -2.7755575615628914e-15 ] ]
* // ]
*
*/
let fft2d = (mat) => {
return _fft2d(mat, 'norm')
}
/**
* iFFT2D
*
* @param {Array} mat 輸入二維[re,im]複數矩陣
* @return {Array} 回傳二維實數矩陣
* @example
*
* let mat
* let res
*
* mat = [
* [ [ 21, 1.0445074572148558e-16 ], [ -2.9999999999999982, 1.7320508075688785 ], [ -3.0000000000000013, -1.7320508075688705 ] ],
* [ [ -9, -2.326366143623307e-16 ], [ 0, -8.881784197001252e-16 ], [ 0, -2.7755575615628914e-15 ] ]
* ]
* res = wf.ifft2d(mat)
* console.log(res)
* // => [
* // [ 1.0000000000000002, 2.0000000000000004, 2.9999999999999987 ],
* // [ 4, 5.000000000000001, 5.999999999999999 ]
* // ]
*
*/
let ifft2d = (mat) => {
return _fft2d(mat, 'inv')
}
/**
* 振幅頻譜1D
*
* @param {Array} arr 輸入數據陣列
* @param {Number} dt 時間間隔,單位s
* @param {Object} [opt={}] 選項物件
* @param {Boolean} [opt.useOneTurn=true] true=輸入視為一個完整週期(頭尾重複),週期(n-1)*dt;false=標準DFT,週期n*dt
* @return {Array} 回傳[{freq,real,imag,ampl},...]頻譜陣列
* @example
*
* let arr
* let res
*
* arr = [1, 2, 3, 4]
* res = wf.spectrum1d(arr, 1)
* console.log(res)
* // => [
* // { freq: 0, real: 10, imag: 0, ampl: 10 },
* // { freq: 0.3333333333333333, real: -2, imag: 2, ampl: 2.8284271247461903 }
* // ]
*
*/
function spectrum1d(arr, dt, opt = {}) {
//check dt
if (!ispnum(dt)) {
throw new Error(`dt[${dt}] is not a positive number`)
}
dt = cdbl(dt)
//useOneTurn: true(預設)=輸入視為一個完整週期(頭尾為同一相位、重複), 週期取(n-1)*dt; false=標準DFT, 週期取n*dt
let useOneTurn = get(opt, 'useOneTurn', true)
//fft1d
let rm = fft1d(arr)
// console.log('rm', rm)
// console.log('n1', size(arr))
//n
let n = size(rm)
// console.log('n', n)
//T, useOneTurn=true時週期(n-1)*dt, =false時標準DFT週期n*dt
let T = useOneTurn ? dt * (n - 1) : dt * n
// console.log('T', T)
//df
let df = 1 / T
// console.log('df', df)
//F
let F = df * (n - 1)
// console.log('F', F)
//hzs
// let hzs = range(df, F + df, df)
let hzs = range(0, F, df)
let hzsHalf = take(hzs, n / 2)
// hzs = [
// ...hzsHalf,
// ...reverse(hzsHalf),
// ]
hzs = hzsHalf
// console.log('hzs', JSON.stringify(hzs), size(hzs))
//rs
let rs = []
each(hzs, (v, k) => {
let freq = v
let real = rm[k][0]
let imag = rm[k][1]
let ampl = Math.sqrt(rm[k][0] ** 2 + rm[k][1] ** 2)
let r = {
freq,
real,
imag,
ampl,
}
rs.push(r)
})
return rs
}
/**
* FFT1D Filter
*
* @param {Array} arr 輸入數據陣列
* @param {Number} dt 輸入數據點時間間隔數字,單位s
* @param {Number} hzStart 輸入過濾用帶通頻率下限數字,單位Hz
* @param {Number} hzEnd 輸入過濾用帶通頻率上限數字,單位Hz
* @param {Object} [opt={}] 選項物件
* @param {Boolean} [opt.useOneTurn=true] true=輸入視為一個完整週期(頭尾重複),週期(n-1)*dt;false=標準DFT,週期n*dt
* @return {Array} 回傳帶通處理後數據陣列
* @example
*
* let dt
* let arr
* let res
*
* //dt=0.0078125s, 3+6hz
* dt = 0.0078125
*
* arr = [
* 0,
* 0.437015151709824,
* 0.845854910274064,
* 1.20056554679302,
* 1.47944976553089,
* 1.66674368151922,
* 1.75379573376597,
* 1.73964987434863,
* 1.63098631369783,
* 1.44142799002054,
* 1.19027504868833,
* 0.900778315875612,
* 0.598101848038141,
* 0.307150781019375,
* 0.0504516520458098,
* -0.153732804251563,
* -0.292893218813452,
* -0.361241031239776,
* -0.360072875476548,
* -0.297503430771426,
* -0.187593110348962,
* -0.0489494660021425,
* 0.0970731816865672,
* 0.228416556922734,
* 0.324423348821458,
* 0.367818520155133,
* 0.346391996239585,
* 0.254233601317238,
* 0.0924099202087415,
* -0.130978839760706,
* -0.401370102712605,
* -0.698891832710318,
* -1,
* -1.27946118721924,
* -1.51251056875181,
* -1.67699974648618,
* -1.75534914481383,
* -1.73613585202716,
* -1.61517856456688,
* -1.39602400854158,
* -1.08979021355164,
* -0.714376916729265,
* -0.293107462345689,
* 0.147084814656977,
* 0.577773754381215,
* 0.971283137555866,
* 1.30286634912854,
* 1.55263964022464,
* 1.70710678118655,
* 1.76014786721285,
* 1.7133908766509,
* 1.57593734934667,
* 1.36346871276832,
* 1.09681259653473,
* 0.80009440465607,
* 0.498634516368545,
* 0.216772751324739,
* -0.0241926543480825,
* -0.207774827040493,
* -0.323625771825179,
* -0.368309299491684,
* -0.345455359932455,
* -0.265285555765141,
* -0.1435542027991,
* -3.67544536472586E-16,
* 0.1435542027991,
* 0.26528555576514,
* 0.345455359932455,
* 0.368309299491684,
* 0.323625771825179,
* 0.207774827040493,
* 0.0241926543480835,
* -0.216772751324738,
* -0.498634516368544,
* -0.800094404656069,
* -1.09681259653473,
* -1.36346871276832,
* -1.57593734934667,
* -1.7133908766509,
* -1.76014786721285,
* -1.70710678118655,
* -1.55263964022464,
* -1.30286634912855,
* -0.971283137555864,
* -0.577773754381218,
* -0.14708481465698,
* 0.293107462345686,
* 0.714376916729258,
* 1.08979021355163,
* 1.39602400854158,
* 1.61517856456688,
* 1.73613585202716,
* 1.75534914481383,
* 1.67699974648618,
* 1.51251056875181,
* 1.27946118721924,
* 1,
* 0.698891832710321,
* 0.40137010271261,
* 0.130978839760704,
* -0.0924099202087405,
* -0.254233601317238,
* -0.346391996239584,
* -0.367818520155133,
* -0.324423348821457,
* -0.228416556922734,
* -0.0970731816865679,
* 0.0489494660021418,
* 0.18759311034896,
* 0.297503430771423,
* 0.360072875476548,
* 0.361241031239776,
* 0.292893218813452,
* 0.153732804251566,
* -0.0504516520458086,
* -0.307150781019376,
* -0.598101848038137,
* -0.900778315875611,
* -1.19027504868833,
* -1.44142799002054,
* -1.63098631369783,
* -1.73964987434863,
* -1.75379573376597,
* -1.66674368151922,
* -1.47944976553089,
* -1.20056554679302,
* -0.845854910274064,
* -0.43701515170983,
* ]
*
* res = wf.filter1d(arr, dt, 0, 2)
* console.log(res)
* // => [
* // -1.967983691641977e-17,
* // -2.307740440357446e-17,
* // -2.6914726362424797e-17,
* // -3.118255834826216e-17,
* // -3.587061878001477e-17,
* // -4.0967613709476845e-17,
* // -4.646126402941945e-17,
* // -5.2338335055034716e-17,
* // -5.858466840745145e-17,
* // -6.518521612250948e-17,
* // -7.212407690262343e-17,
* // -7.938453442440174e-17,
* // -8.694909760973329e-17,
* // -9.479954276332726e-17,
* // -1.029169574751909e-16,
* // -1.112817861822817e-16,
* // ... (112 more items)
* // ]
*
* res = wf.filter1d(arr, dt, 0, 4)
* console.log(res)
* // => [
* // -4.560690164444523e-16,
* // 0.14673047445536136,
* // 0.290284677254462,
* // 0.4275550934302818,
* // 0.5555702330196021,
* // 0.6715589548470182,
* // 0.7730104533627369,
* // 0.8577286100002721,
* // 0.9238795325112868,
* // 0.970031253194544,
* // 0.9951847266721972,
* // 0.9987954562051725,
* // 0.9807852804032308,
* // 0.9415440651830213,
* // 0.8819212643483555,
* // 0.8032075314806454,
* // ... (112 more items)
* // ]
*
* res = wf.filter1d(arr, dt, 4, 8)
* console.log(res)
* // => [
* // -4.482834651769285e-16,
* // 0.2902846772544623,
* // 0.5555702330196025,
* // 0.7730104533627374,
* // 0.9238795325112875,
* // 0.995184726672198,
* // 0.9807852804032318,
* // 0.8819212643483564,
* // 0.7071067811865488,
* // 0.47139673682599875,
* // 0.19509032201612916,
* // -0.0980171403295599,
* // -0.3826834323650895,
* // -0.6343932841636456,
* // -0.8314696123025458,
* // -0.9569403357322097,
* // ... (112 more items)
* // ]
*
*/
function filter1d(arr, dt, hzStart, hzEnd, opt = {}) {
//check dt
if (!ispnum(dt)) {
throw new Error(`dt[${dt}] is not a positive number`)
}
dt = cdbl(dt)
//check hzStart
if (!isp0num(hzStart)) {
throw new Error(`hzStart[${hzStart}] is not a positive number`)
}
hzStart = cdbl(hzStart)
//check hzEnd
if (!isp0num(hzEnd)) {
throw new Error(`hzEnd[${hzEnd}] is not a positive number`)
}
hzEnd = cdbl(hzEnd)
//useOneTurn: true(預設)=輸入視為一個完整週期(頭尾重複), 週期(n-1)*dt; false=標準DFT, 週期n*dt
let useOneTurn = get(opt, 'useOneTurn', true)
//fft1d
let rm = fft1d(arr)
// console.log('rm', rm)
// console.log('n1', size(arr))
//n
let n = size(rm)
// console.log('n', n)
//T
let T = useOneTurn ? dt * (n - 1) : dt * n
// console.log('T', T)
//df
let df = 1 / T
// console.log('df', df)
//F
// let F = df * (n - 1) //舊版hzs建構用, 新遮罩改用min(k,n-k)不需要
// console.log('F', F)
//hzs
//舊版: 對稱建構hzs; 對奇數n時 take(hzs, n/2) 取整少一個 -> mirror後長度n-1, 末尾bin永遠漏遮罩, 且負頻半段與正頻不共軛對齊 -> 輸出非實數(被ifft取實部截斷), 保留供比對
// let hzs = range(df, F + df, df)
// let hzs = range(0, F, df)
// let hzsHalf = take(hzs, n / 2)
// hzs = [
// ...hzsHalf,
// ...reverse(hzsHalf),
// ]
//新版: 每個bin k的頻率大小=min(k,n-k)*df(bin k 與其共軛 bin n-k 同頻率→一起遮罩, 輸出保持實數), 涵蓋全部n個bin, 奇偶n皆正確
let hzs = range(0, n).map((k) => Math.min(k, n - k) * df)
// console.log('hzs', JSON.stringify(hzs), size(hzs))
//帶通, 依照指定起訖頻率清除rm
each(hzs, (v, k) => {
let b = hzStart <= v && v <= hzEnd //允許通過
if (!b) {
rm[k][0] = 0
rm[k][1] = 0
}
})
// console.log('rm(clean)', JSON.stringify(rm))
//ifft1d
let res = ifft1d(rm)
// console.log('res', res)
return res
}
/**
* 振幅頻譜2D
*
* @param {Array} mat 輸入二維實數矩陣
* @param {Number} dt 時間間隔,單位s
* @param {Object} [opt={}] 選項物件
* @param {Boolean} [opt.useOneTurn=true] true=各軸視為一個完整週期(頭尾重複),週期(len-1)*dt;false=標準DFT,週期len*dt
* @return {Array} 回傳二維[{freqRow,freqCol,real,imag,ampl},...]頻譜矩陣(僅正頻半象限)
* @example
*
* let mat
* let res
*
* mat = [[0,1,2,3],[1,2,3,4],[2,3,4,5],[3,4,5,6]]
* res = wf.spectrum2d(mat, 0.5)
* console.log(res)
* // => [
* // [
* // { freqRow: 0, freqCol: 0, real: 48, imag: 0, ampl: 48 },
* // { freqRow: 0, freqCol: 0.6666666666666666, real: -8, imag: 8, ampl: 11.313708498984761 }
* // ],
* // [
* // { freqRow: 0.6666666666666666, freqCol: 0, real: -8, imag: 8, ampl: 11.313708498984761 },
* // { freqRow: 0.6666666666666666, freqCol: 0.6666666666666666, real: 0, imag: 0, ampl: 0 }
* // ]
* // ]
*
*/
function spectrum2d(mat, dt, opt = {}) {
//check dt
if (!ispnum(dt)) {
throw new Error(`dt[${dt}] is not a positive number`)
}
dt = cdbl(dt)
//useOneTurn: true(預設)=各軸視為一個完整週期(頭尾重複), 週期(len-1)*dt; false=標準DFT, 週期len*dt
let useOneTurn = get(opt, 'useOneTurn', true)
//fft2d, 回傳m×n之[re,im]
let rm = fft2d(mat)
//m, n (列數, 行數)
let m = size(rm)
let n = size(get(rm, 0, []))
//df, 兩軸各自以該軸點數計算
let dfRow = 1 / ((useOneTurn ? m - 1 : m) * dt)
let dfCol = 1 / ((useOneTurn ? n - 1 : n) * dt)
//取正頻半象限 floor(m/2) × floor(n/2)(與1D取floor(n/2)一致)
let mHalf = Math.floor(m / 2)
let nHalf = Math.floor(n / 2)
//rs, 每格{freqRow, freqCol, real, imag, ampl}
let rs = []
for (let i = 0; i < mHalf; i++) {
let row = []
for (let j = 0; j < nHalf; j++) {
let real = rm[i][j][0]
let imag = rm[i][j][1]
let ampl = Math.sqrt(real ** 2 + imag ** 2)
row.push({
freqRow: i * dfRow,
freqCol: j * dfCol,
real,
imag,
ampl,
})
}
rs.push(row)
}
return rs
}
/**
* FFT2D 帶通濾波
*
* @param {Array} mat 輸入二維實數矩陣
* @param {Number} dt 時間間隔,單位s
* @param {Number} hzStart 帶通頻率下限,單位Hz(以徑向頻率 sqrt(fRow²+fCol²) 判斷)
* @param {Number} hzEnd 帶通頻率上限,單位Hz
* @param {Object} [opt={}] 選項物件
* @param {Boolean} [opt.useOneTurn=true] true=各軸視為一個完整週期(頭尾重複),週期(len-1)*dt;false=標準DFT,週期len*dt
* @return {Array} 回傳帶通處理後二維實數矩陣
* @example
*
* let mat
* let res
*
* mat = [[0,1,2,3],[1,2,3,4],[2,3,4,5],[3,4,5,6]]
* res = wf.filter2d(mat, 0.5, 0.3, 0.8)
* console.log(res)
* // => [
* // [ -2, -2, 0, 0 ],
* // [ -2, -2, 0, 0 ],
* // [ 0, 0, 2, 2 ],
* // [ 0, 0, 2, 2 ]
* // ]
*
*/
function filter2d(mat, dt, hzStart, hzEnd, opt = {}) {
//check dt
if (!ispnum(dt)) {
throw new Error(`dt[${dt}] is not a positive number`)
}
dt = cdbl(dt)
//check hzStart
if (!isp0num(hzStart)) {
throw new Error(`hzStart[${hzStart}] is not a positive number`)
}
hzStart = cdbl(hzStart)
//check hzEnd
if (!isp0num(hzEnd)) {
throw new Error(`hzEnd[${hzEnd}] is not a positive number`)
}
hzEnd = cdbl(hzEnd)
//useOneTurn: true(預設)=各軸視為一個完整週期(頭尾重複), 週期(len-1)*dt; false=標準DFT, 週期len*dt
let useOneTurn = get(opt, 'useOneTurn', true)
//fft2d, 回傳m×n之[re,im]
let rm = fft2d(mat)
//m, n
let m = size(rm)
let n = size(get(rm, 0, []))
//df, 兩軸各自
let dfRow = 1 / ((useOneTurn ? m - 1 : m) * dt)
let dfCol = 1 / ((useOneTurn ? n - 1 : n) * dt)
//帶通, 以徑向頻率sqrt(fRow²+fCol²)判斷; bin(i,j)頻率大小取min(i,m-i)*dfRow與min(j,n-j)*dfCol(與共軛bin同值→一起遮罩, 輸出保持實數), 涵蓋全部bin
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
let fRow = Math.min(i, m - i) * dfRow
let fCol = Math.min(j, n - j) * dfCol
let fRad = Math.sqrt(fRow ** 2 + fCol ** 2)
let b = hzStart <= fRad && fRad <= hzEnd //允許通過
if (!b) {
rm[i][j][0] = 0
rm[i][j][1] = 0
}
}
}
//ifft2d
let res = ifft2d(rm)
return res
}
/**
* FFT與iFFT
*
* @returns {Array} 回傳數據陣列
* @example
*
* let arr
* let res
*
* arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
* res = wf.fft1d(arr)
* console.log(res)
* // => [
* // [ 120, 0 ],
* // [ -8, 40.21871593700678 ],
* // [ -8, 19.31370849898476 ],
* // [ -8.000000000000002, 11.972846101323912 ],
* // [ -8, 8 ],
* // [ -7.999999999999999, 5.345429103354391 ],
* // [ -8, 3.3137084989847594 ],
* // [ -8.000000000000002, 1.5912989390372623 ],
* // [ -8, 0 ],
* // [ -8, -1.5912989390372623 ],
* // [ -7.999999999999999, -3.3137084989847594 ],
* // [ -7.999999999999998, -5.345429103354393 ],
* // [ -7.999999999999999, -8 ],
* // [ -7.999999999999999, -11.97284610132391 ],
* // [ -7.999999999999997, -19.31370849898476 ],
* // [ -7.999999999999993, -40.21871593700678 ]
* // ]
*
* arr = [
* [120, 0],
* [-8, 40.21871593700678],
* [-8, 19.31370849898476],
* [-7.999999999999999, 11.972846101323913],
* [-8, 8],
* [-8, 5.345429103354391],
* [-8, 3.313708498984761],
* [-7.999999999999999, 1.5912989390372623],
* [-8, 0],
* [-7.999999999999999, -1.5912989390372623],
* [-8, -3.313708498984761],
* [-8, -5.345429103354391],
* [-8, -8],
* [-7.999999999999999, -11.972846101323913],
* [-8, -19.31370849898476],
* [-8, -40.21871593700678]
* ]
* res = wf.ifft1d(arr)
* console.log(res)
* // => [
* // 0, 1,
* // 2.0000000000000004, 3.0000000000000013,
* // 4, 5.000000000000002,
* // 6, 7.000000000000001,
* // 8, 9,
* // 10, 10.999999999999998,
* // 12, 12.999999999999998,
* // 14, 15
* // ]
*
*/
let WFft = {
fft1d,
ifft1d,
fft2d,
ifft2d,
spectrum1d,
spectrum2d,
filter1d,
filter2d,
}
export default WFft