refactor: add fixed-point coefficient overrides and enhance manual adjustment tracking
This commit is contained in:
+230
-49
@@ -6,13 +6,13 @@ createApp({
|
||||
fs: 100000.0,
|
||||
b_str: "0.5, 0.5, 0.0, 0.0",
|
||||
a_str: "1.0, 0.0, 0.0, 0.0",
|
||||
filterType: "(無) 手動自訂",
|
||||
filterType: "Lowpass (低通)",
|
||||
filterOptions: [
|
||||
"(無) 手動自訂", "Lowpass (低通)", "Highpass (高通)",
|
||||
"Bandpass (帶通)", "Notch (陷波器)", "1P1Z (一極一零)",
|
||||
"2P2Z (二極二零)", "PID 控制器", "SOGI-Alpha (帶通)", "SOGI-Beta (低通)"
|
||||
"Lowpass (低通)", "Highpass (高通)", "Bandpass (帶通)",
|
||||
"Notch (陷波器)", "1P1Z (一極一零)", "2P2Z (二極二零)",
|
||||
"PID 控制器", "SOGI-Alpha (帶通)", "SOGI-Beta (低通)", "(無) 手動自訂"
|
||||
],
|
||||
systemGainDB: 0.0,
|
||||
systemGain: 1.0,
|
||||
params: {
|
||||
fc: 1000.0, order: 1,
|
||||
bp_f_low: 500.0, bp_f_high: 2000.0, bp_order: 1,
|
||||
@@ -35,13 +35,14 @@ createApp({
|
||||
csvFile: null, csvColumns: [], csvPreview: [],
|
||||
selectedColumn: 0, loadingFilter: false, filterDone: false, timePlotData: null,
|
||||
mobileTab: 'settings', // 手機版頁籤:'settings' | 'chart'
|
||||
shiftBits: 12,
|
||||
shiftBits: 14,
|
||||
fixedOverrides: { a: {}, b: {} },
|
||||
outOfRangeB: [false, false, false, false, false, false, false],
|
||||
outOfRangeA: [false, false, false, false, false, false, false],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
linearSystemGain() {
|
||||
return Math.pow(10, this.systemGainDB / 20.0);
|
||||
},
|
||||
maxLogB() {
|
||||
if (this.sense_b === '1%') return Math.log10(1.01);
|
||||
if (this.sense_b === '2x') return Math.log10(2.0);
|
||||
@@ -50,22 +51,68 @@ createApp({
|
||||
maxLogA() {
|
||||
if (this.sense_a === '1%') return Math.log10(1.01);
|
||||
if (this.sense_a === '2x') return Math.log10(2.0);
|
||||
return 1.0;
|
||||
if (this.sense_a === '10x') return Math.log10(10.0);
|
||||
return Math.log10(2.0);
|
||||
},
|
||||
b_int_str: {
|
||||
get() {
|
||||
return this.fixedPointCoeffs.b.map(item => item.val).join(', ');
|
||||
},
|
||||
set(val) {
|
||||
const cleaned = val.replace(/[^0-9,\-\s]/g, '');
|
||||
const nums = this.parseCoeffs(cleaned);
|
||||
nums.forEach((n, i) => {
|
||||
this.fixedOverrides.b[i] = Math.round(n);
|
||||
});
|
||||
}
|
||||
},
|
||||
a_int_str: {
|
||||
get() {
|
||||
return this.fixedPointCoeffs.a.map(item => item.val).join(', ');
|
||||
},
|
||||
set(val) {
|
||||
const cleaned = val.replace(/[^0-9,\-\s]/g, '');
|
||||
const nums = this.parseCoeffs(cleaned);
|
||||
nums.forEach((n, i) => {
|
||||
this.fixedOverrides.a[i] = Math.round(n);
|
||||
});
|
||||
}
|
||||
},
|
||||
fixedPointCoeffs() {
|
||||
const scale = Math.pow(2, this.shiftBits);
|
||||
const b_raw = this.parseCoeffs(this.b_str).map(x => x * this.linearSystemGain);
|
||||
const b_raw = this.parseCoeffs(this.b_str);
|
||||
const a_raw = this.parseCoeffs(this.a_str);
|
||||
|
||||
const b = b_raw.map((x, i) => ({ label: `b${i}`, val: Math.round(x * scale) }));
|
||||
const a = a_raw.map((x, i) => ({ label: `a${i}`, val: Math.round(x * scale) }));
|
||||
const b = b_raw.map((x, i) => {
|
||||
const autoVal = Math.round(x * scale);
|
||||
const val = (this.fixedOverrides.b[i] !== undefined) ? this.fixedOverrides.b[i] : autoVal;
|
||||
return { label: `b${i}`, val, isOverridden: this.fixedOverrides.b[i] !== undefined };
|
||||
});
|
||||
const a = a_raw.map((x, i) => {
|
||||
const autoVal = Math.round(x * scale);
|
||||
const val = (this.fixedOverrides.a[i] !== undefined) ? this.fixedOverrides.a[i] : autoVal;
|
||||
return { label: `a${i}`, val, isOverridden: this.fixedOverrides.a[i] !== undefined };
|
||||
});
|
||||
|
||||
return { b, a };
|
||||
},
|
||||
floatingCoeffs() {
|
||||
const b = this.parseCoeffs(this.b_str).map((x, i) => ({ label: `b${i}`, val: parseFloat((x * this.linearSystemGain).toPrecision(6)) }));
|
||||
const b = this.parseCoeffs(this.b_str).map((x, i) => ({ label: `b${i}`, val: parseFloat(x.toPrecision(6)) }));
|
||||
const a = this.parseCoeffs(this.a_str).map((x, i) => ({ label: `a${i}`, val: parseFloat(x.toPrecision(6)) }));
|
||||
return { b, a };
|
||||
},
|
||||
isManualDirty() {
|
||||
if (this.bSliders.some(v => v !== 0) || this.aSliders.some(v => v !== 0)) return true;
|
||||
const currentB = this.padTo7(this.parseCoeffs(this.b_str));
|
||||
const currentA = this.padTo7(this.parseCoeffs(this.a_str));
|
||||
const targetB = this.baseB.map(x => x * (this.systemGain || 1.0));
|
||||
const targetA = this.baseA;
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
if (Math.abs((currentB[i] || 0) - (targetB[i] || 0)) > 1e-8) return true;
|
||||
if (Math.abs((currentA[i] || 0) - (targetA[i] || 0)) > 1e-8) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -78,8 +125,7 @@ createApp({
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
this.syncBaseFromText();
|
||||
this.updateBodePlot();
|
||||
this.applyFilterDesign();
|
||||
},
|
||||
methods: {
|
||||
toggleDarkMode() {
|
||||
@@ -109,36 +155,95 @@ createApp({
|
||||
return r;
|
||||
},
|
||||
syncBaseFromText() {
|
||||
this.baseB = this.padTo7(this.parseCoeffs(this.b_str));
|
||||
const gain = this.systemGain || 1.0;
|
||||
this.baseB = this.padTo7(this.parseCoeffs(this.b_str)).map(x => x / gain);
|
||||
this.baseA = this.padTo7(this.parseCoeffs(this.a_str));
|
||||
this.baseA[0] = 1.0;
|
||||
this.fixedOverrides = { a: {}, b: {} };
|
||||
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
|
||||
this.aSliders = [0, 0, 0, 0, 0, 0];
|
||||
this.outOfRangeB = [false, false, false, false, false, false, false];
|
||||
this.outOfRangeA = [false, false, false, false, false, false, false];
|
||||
},
|
||||
resetSlidersB() {
|
||||
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
|
||||
this.b_str = this.baseB.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.updateBodePlot();
|
||||
},
|
||||
resetSlidersA() {
|
||||
this.aSliders = [0, 0, 0, 0, 0, 0];
|
||||
this.a_str = this.baseA.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.updateBodePlot();
|
||||
},
|
||||
updateFromSliders() {
|
||||
const currentB = this.baseB.map((base, i) => base * Math.pow(10, this.bSliders[i] || 0));
|
||||
clearManualAdjustments() {
|
||||
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
|
||||
this.aSliders = [0, 0, 0, 0, 0, 0];
|
||||
const gain = this.systemGain || 1.0;
|
||||
this.b_str = this.baseB.map(x => parseFloat((x * gain).toPrecision(10))).join(', ');
|
||||
this.a_str = this.baseA.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
},
|
||||
updateFromControls() {
|
||||
const currentB = this.baseB.map((base, i) => base * (this.systemGain || 1.0) * Math.pow(10, this.bSliders[i] || 0));
|
||||
const currentA = [1.0];
|
||||
for (let i = 1; i < 7; i++) {
|
||||
currentA.push(this.baseA[i] * Math.pow(10, this.aSliders[i] || 0));
|
||||
}
|
||||
this.b_str = currentB.map(x => parseFloat(x.toPrecision(6))).join(', ');
|
||||
this.a_str = currentA.map(x => parseFloat(x.toPrecision(6))).join(', ');
|
||||
this.debouncedUpdateBode();
|
||||
this.b_str = currentB.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.a_str = currentA.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
},
|
||||
updateFromText() {
|
||||
this.syncBaseFromText();
|
||||
// 不再呼叫 syncBaseFromText(),保留基礎設計 (baseB/baseA) 作為參考點
|
||||
// 只更新圖表,讓滑桿維持在相對應的偏差位置
|
||||
this.updateBodePlot();
|
||||
},
|
||||
calcSlidersFromText() {
|
||||
const gain = this.systemGain || 1.0;
|
||||
const currentB = this.parseCoeffs(this.b_str);
|
||||
const currentA = this.parseCoeffs(this.a_str);
|
||||
|
||||
// 計算 b 係數滑桿
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const val = currentB[i] || 0;
|
||||
const base = this.baseB[i] || 0;
|
||||
this.outOfRangeB[i] = false;
|
||||
if (base !== 0 && val !== 0 && (val/(base*gain)) > 0) {
|
||||
const logVal = Math.log10(val / (base * gain));
|
||||
if (Math.abs(logVal) > this.maxLogB) this.outOfRangeB[i] = true;
|
||||
this.bSliders[i] = Math.max(-this.maxLogB, Math.min(this.maxLogB, logVal));
|
||||
} else {
|
||||
this.bSliders[i] = 0;
|
||||
if (val !== 0 && base !== 0) this.outOfRangeB[i] = true; // 符號不同或極端
|
||||
}
|
||||
}
|
||||
// 計算 a 係數滑桿 (a0 恆為 1, 從 a1 開始)
|
||||
for (let i = 1; i < 7; i++) {
|
||||
const val = currentA[i] || 0;
|
||||
const base = this.baseA[i] || 0;
|
||||
this.outOfRangeA[i] = false;
|
||||
if (base !== 0 && val !== 0 && (val/base) > 0) {
|
||||
const logVal = Math.log10(val / base);
|
||||
if (Math.abs(logVal) > this.maxLogA) this.outOfRangeA[i] = true;
|
||||
this.aSliders[i] = Math.max(-this.maxLogA, Math.min(this.maxLogA, logVal));
|
||||
} else {
|
||||
this.aSliders[i] = 0;
|
||||
if (val !== 0 && base !== 0) this.outOfRangeA[i] = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
debouncedUpdateFromText(type) {
|
||||
if (type === 'b') this.b_str = this.b_str.replace(/[^0-9,.\-\s]/g, '');
|
||||
if (type === 'a') this.a_str = this.a_str.replace(/[^0-9,.\-\s]/g, '');
|
||||
|
||||
// 當文字改變時,反向計算滑桿位置
|
||||
this.calcSlidersFromText();
|
||||
},
|
||||
updateFromFixed(idx, type, val) {
|
||||
const intVal = parseInt(val);
|
||||
if (isNaN(intVal)) return;
|
||||
this.fixedOverrides[type][idx] = intVal;
|
||||
this.updateBodePlot();
|
||||
},
|
||||
clearFixedOverrides() {
|
||||
this.fixedOverrides = { a: {}, b: {} };
|
||||
},
|
||||
onFsChange() {
|
||||
if (this.filterType !== "(無) 手動自訂") {
|
||||
this.applyFilterDesign();
|
||||
@@ -157,6 +262,45 @@ createApp({
|
||||
clearTimeout(this.bodeTimeout);
|
||||
this.bodeTimeout = setTimeout(() => this.updateBodePlot(), 150);
|
||||
},
|
||||
onGainChange() {
|
||||
this.updateFromControls();
|
||||
this.debouncedUpdateBode();
|
||||
},
|
||||
onFilterTypeChange() {
|
||||
this.systemGain = 1.0;
|
||||
// 這裡可以選擇是否重設所有設計參數 (fc, Q 等)
|
||||
// 根據使用者要求「載入預設值」,我們呼叫重設邏輯
|
||||
if (this.filterType !== "(無) 手動自訂") {
|
||||
this.params = {
|
||||
fc: 1000.0, order: 1,
|
||||
bp_f_low: 500.0, bp_f_high: 2000.0, bp_order: 1,
|
||||
notch_f0: 120.0, notch_q: 1.0,
|
||||
opoz_fz: 15000.0, opoz_fp: 10.0,
|
||||
tptz_fz1: 200.0, tptz_fz2: 25000.0, tptz_fp1: 10.0, tptz_fp2: 5000.0,
|
||||
kp: 0.003, ki: 10.0, kd: 0.000016,
|
||||
sogi_f0: 60.0, sogi_k: 1.414,
|
||||
};
|
||||
}
|
||||
this.applyFilterDesign();
|
||||
},
|
||||
resetFilterParams() {
|
||||
if (this.filterType === "(無) 手動自訂") {
|
||||
this.b_str = "1.0, 0.0, 0.0, 0.0";
|
||||
this.a_str = "1.0, 0.0, 0.0, 0.0";
|
||||
this.updateFromText();
|
||||
} else {
|
||||
this.params = {
|
||||
fc: 1000.0, order: 1,
|
||||
bp_f_low: 500.0, bp_f_high: 2000.0, bp_order: 1,
|
||||
notch_f0: 120.0, notch_q: 1.0,
|
||||
opoz_fz: 15000.0, opoz_fp: 10.0,
|
||||
tptz_fz1: 200.0, tptz_fz2: 25000.0, tptz_fp1: 10.0, tptz_fp2: 5000.0,
|
||||
kp: 0.003, ki: 10.0, kd: 0.000016,
|
||||
sogi_f0: 60.0, sogi_k: 1.414,
|
||||
};
|
||||
this.applyFilterDesign();
|
||||
}
|
||||
},
|
||||
async applyFilterDesign() {
|
||||
if (this.filterType === "(無) 手動自訂") return;
|
||||
this.globalError = null;
|
||||
@@ -179,9 +323,17 @@ createApp({
|
||||
});
|
||||
if (!res.ok) { const err = await res.json(); throw new Error(err.detail || '設計濾波器失敗'); }
|
||||
const data = await res.json();
|
||||
this.b_str = data.b.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.a_str = data.a.map(x => parseFloat(x.toPrecision(10))).join(', ');
|
||||
this.syncBaseFromText();
|
||||
// 1. 將後端傳回的單位增益係數存入 baseB
|
||||
this.baseB = this.padTo7(data.b);
|
||||
this.baseA = this.padTo7(data.a);
|
||||
this.baseA[0] = 1.0;
|
||||
|
||||
// 2. 重新計算含增益的文字框顯示與滑桿位置
|
||||
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
|
||||
this.aSliders = [0, 0, 0, 0, 0, 0];
|
||||
this.updateFromControls();
|
||||
|
||||
// 3. 更新圖表
|
||||
this.updateBodePlot();
|
||||
} catch (e) { this.globalError = e.message; }
|
||||
},
|
||||
@@ -189,23 +341,44 @@ createApp({
|
||||
this.loadingBode = true;
|
||||
this.globalError = null;
|
||||
try {
|
||||
let b = this.parseCoeffs(this.b_str).map(val => val * this.linearSystemGain);
|
||||
let a = this.parseCoeffs(this.a_str);
|
||||
if (b.length === 0) b = [1.0];
|
||||
if (a.length === 0) a = [1.0];
|
||||
const res = await fetch('/api/bode', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ b, a, fs: this.fs })
|
||||
let b_ideal = this.parseCoeffs(this.b_str);
|
||||
let a_ideal = this.parseCoeffs(this.a_str);
|
||||
if (b_ideal.length === 0) b_ideal = [1.0];
|
||||
if (a_ideal.length === 0) a_ideal = [1.0];
|
||||
|
||||
const scale = Math.pow(2, this.shiftBits);
|
||||
const b_fixed = b_ideal.map((x, i) => {
|
||||
const intVal = (this.fixedOverrides.b[i] !== undefined) ? this.fixedOverrides.b[i] : Math.round(x * scale);
|
||||
return intVal / scale;
|
||||
});
|
||||
if (!res.ok) throw new Error('無法計算頻率響應');
|
||||
const data = await res.json();
|
||||
this.drawBodePlot(data.freq, data.mag, data.phase);
|
||||
const a_fixed = a_ideal.map((x, i) => {
|
||||
const intVal = (this.fixedOverrides.a[i] !== undefined) ? this.fixedOverrides.a[i] : Math.round(x * scale);
|
||||
return intVal / scale;
|
||||
});
|
||||
|
||||
const [resIdeal, resFixed] = await Promise.all([
|
||||
fetch('/api/bode', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ b: b_ideal, a: a_ideal, fs: this.fs })
|
||||
}),
|
||||
fetch('/api/bode', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ b: b_fixed, a: a_fixed, fs: this.fs })
|
||||
})
|
||||
]);
|
||||
|
||||
if (!resIdeal.ok || !resFixed.ok) throw new Error('無法計算頻率響應');
|
||||
|
||||
const dataIdeal = await resIdeal.json();
|
||||
const dataFixed = await resFixed.json();
|
||||
|
||||
this.drawBodePlot(dataIdeal.freq, dataIdeal.mag, dataIdeal.phase, dataFixed.mag, dataFixed.phase);
|
||||
// 手機版:計算完成後自動切換到圖表頁籤
|
||||
if (window.innerWidth < 1024) this.mobileTab = 'chart';
|
||||
} catch (e) { this.globalError = e.message; }
|
||||
finally { this.loadingBode = false; }
|
||||
},
|
||||
drawBodePlot(freq, mag, phase) {
|
||||
drawBodePlot(freq, magIdeal, phaseIdeal, magFixed, phaseFixed) {
|
||||
const fs = this.fs;
|
||||
const fMin = freq[0], fMax = freq[freq.length - 1];
|
||||
const isDark = this.isDarkMode;
|
||||
@@ -283,12 +456,19 @@ createApp({
|
||||
const layout = {
|
||||
paper_bgcolor: 'transparent', plot_bgcolor: 'transparent',
|
||||
margin: isSmallScreen ? { t: 80, b: 70, l: 55, r: 15 } : { t: 70, b: 60, l: 70, r: 30 },
|
||||
showlegend: false,
|
||||
showlegend: true,
|
||||
legend: {
|
||||
orientation: 'h',
|
||||
y: 1.1,
|
||||
x: 0.5,
|
||||
xanchor: 'center',
|
||||
font: { size: isSmallScreen ? 10 : 12, color: textColor }
|
||||
},
|
||||
grid: { rows: 2, columns: 1, pattern: 'independent', roworder: 'top to bottom', ygap: isSmallScreen ? 0.35 : 0.35 },
|
||||
font: { color: textColor, family: 'Inter, sans-serif' },
|
||||
annotations: [
|
||||
{ text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.22 : 1.18, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
|
||||
{ text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.22 : 1.18, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
|
||||
{ text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
|
||||
{ text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
|
||||
...annotations
|
||||
],
|
||||
xaxis: { ...xAxisCommon },
|
||||
@@ -298,10 +478,13 @@ createApp({
|
||||
shapes: shapes
|
||||
};
|
||||
|
||||
const traceMag = { x: freq, y: mag, name: 'Magnitude (dB)', type: 'scatter', line: { color: isDark ? '#3b82f6' : '#2563eb', width: 2.5 } };
|
||||
const tracePhase = { x: freq, y: phase, name: 'Phase (deg)', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2' };
|
||||
const traceMagIdeal = { x: freq, y: magIdeal, name: 'Ideal (Float)', type: 'scatter', line: { color: isDark ? '#3b82f6' : '#2563eb', width: 2.5 } };
|
||||
const traceMagFixed = { x: freq, y: magFixed, name: `Fixed (Q${this.shiftBits})`, type: 'scatter', line: { color: isDark ? '#10b981' : '#059669', width: 2, dash: 'dot' } };
|
||||
|
||||
const tracePhaseIdeal = { x: freq, y: phaseIdeal, name: 'Ideal Phase', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2', showlegend: false };
|
||||
const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2', showlegend: false };
|
||||
|
||||
Plotly.react('bodePlot', [traceMag, tracePhase], layout, { responsive: true });
|
||||
Plotly.react('bodePlot', [traceMagIdeal, traceMagFixed, tracePhaseIdeal, tracePhaseFixed], layout, { responsive: true });
|
||||
},
|
||||
|
||||
handleFileUpload(event) {
|
||||
@@ -338,9 +521,8 @@ createApp({
|
||||
this.loadingFilter = true;
|
||||
this.globalError = null;
|
||||
const formData = new FormData();
|
||||
const final_b_str = this.parseCoeffs(this.b_str).map(val => val * this.linearSystemGain).join(', ');
|
||||
formData.append('file', this.csvFile);
|
||||
formData.append('b', final_b_str);
|
||||
formData.append('b', this.b_str);
|
||||
formData.append('a', this.a_str);
|
||||
formData.append('col_idx', this.selectedColumn);
|
||||
try {
|
||||
@@ -378,9 +560,8 @@ createApp({
|
||||
async downloadCsv() {
|
||||
if (!this.csvFile) return;
|
||||
const formData = new FormData();
|
||||
const final_b_str = this.parseCoeffs(this.b_str).map(val => val * this.linearSystemGain).join(', ');
|
||||
formData.append('file', this.csvFile);
|
||||
formData.append('b', final_b_str);
|
||||
formData.append('b', this.b_str);
|
||||
formData.append('a', this.a_str);
|
||||
formData.append('col_idx', this.selectedColumn);
|
||||
try {
|
||||
|
||||
+91
-53
@@ -82,17 +82,21 @@
|
||||
<!-- 濾波器設計工具 -->
|
||||
<section>
|
||||
<h2
|
||||
class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
|
||||
</path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
濾波器設計
|
||||
class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
|
||||
</path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
濾波器設計
|
||||
</div>
|
||||
<button @click="resetFilterParams"
|
||||
class="text-[10px] font-bold bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800/50 px-2 py-1 rounded transition-colors uppercase">重設設計</button>
|
||||
</h2>
|
||||
<select v-model="filterType" @change="applyFilterDesign"
|
||||
<select v-model="filterType" @change="onFilterTypeChange"
|
||||
class="w-full bg-slate-50 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all mb-3 text-slate-900 dark:text-gray-100 truncate">
|
||||
<option v-for="opt in filterOptions" :key="opt" :value="opt">{{ opt }}</option>
|
||||
</select>
|
||||
@@ -230,12 +234,12 @@
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 flex justify-between mb-1"><span>取樣頻率 fs
|
||||
(Hz)</span><span class="text-emerald-600 dark:text-emerald-400">{{ formatK(fs)
|
||||
}}</span></label>
|
||||
<input type="number" v-model.number="fs" @change="onFsChange"
|
||||
<input type="number" v-model.number="fs" @input="debouncedApply"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 flex justify-between mb-1"><span>系統整體增益 (System Gain)</span><span class="text-blue-600 dark:text-blue-400">{{ systemGainDB }} dB (線性倍率: {{ linearSystemGain.toFixed(4) }}x)</span></label>
|
||||
<input type="number" v-model.number="systemGainDB" @change="updateBodePlot(); if(filterDone) processFilter();" step="1"
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 flex justify-between mb-1"><span>系統增益 K (System Gain)</span><span class="text-blue-600 dark:text-blue-400">{{ systemGain }}x</span></label>
|
||||
<input type="number" v-model.number="systemGain" @input="onGainChange" step="0.1"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,8 +252,13 @@
|
||||
<h2
|
||||
class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
|
||||
<span>濾波器係數設定</span>
|
||||
<button @click="updateFromText"
|
||||
class="text-xs font-semibold bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 px-3 py-1.5 rounded text-slate-600 dark:text-gray-300 transition-colors shadow-sm">套用</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="isManualDirty"
|
||||
@click="clearManualAdjustments"
|
||||
class="text-[10px] font-bold bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 px-2 py-1 rounded text-slate-500 transition-colors uppercase mr-1">重設</button>
|
||||
<button @click="updateFromText"
|
||||
class="text-[10px] font-bold bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded transition-colors uppercase shadow-sm">套用</button>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="mb-3">
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">前饋分子係數 b (b0 ~ b6)</label>
|
||||
@@ -260,7 +269,7 @@
|
||||
<span class="text-[10px] font-mono text-blue-800 dark:text-blue-300">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea v-model="b_str" @change="updateFromText" rows="2"
|
||||
<textarea v-model="b_str" @input="debouncedUpdateFromText('b')" rows="2"
|
||||
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-blue-700 dark:text-blue-300"></textarea>
|
||||
</div>
|
||||
<!-- b 係數倍率微調 -->
|
||||
@@ -281,9 +290,11 @@
|
||||
</div>
|
||||
<template v-for="i in 7" :key="'bs'+i">
|
||||
<div v-if="baseB[i-1] !== 0">
|
||||
<label class="text-xs text-slate-400 dark:text-gray-500 block mt-1">b{{ i-1 }} (基準:
|
||||
{{ baseB[i-1].toPrecision(6) }})</label>
|
||||
<input type="range" v-model.number="bSliders[i-1]" @input="updateFromSliders"
|
||||
<label class="text-xs text-slate-400 dark:text-gray-500 flex justify-between mt-1">
|
||||
<span>b{{ i-1 }} (基準: {{ (baseB[i-1] * (systemGain || 1.0)).toPrecision(6) }})</span>
|
||||
<span v-if="outOfRangeB[i-1]" class="text-amber-500 font-bold animate-pulse">⚠️ 超出滑桿範圍</span>
|
||||
</label>
|
||||
<input type="range" v-model.number="bSliders[i-1]" @input="updateFromControls"
|
||||
:min="-maxLogB" :max="maxLogB" :step="maxLogB/100" class="w-full accent-blue-500">
|
||||
</div>
|
||||
</template>
|
||||
@@ -298,7 +309,7 @@
|
||||
<span class="text-[10px] font-mono text-orange-800 dark:text-orange-300">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea v-model="a_str" @change="updateFromText" rows="2"
|
||||
<textarea v-model="a_str" @input="debouncedUpdateFromText('a')" rows="2"
|
||||
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-orange-700 dark:text-orange-300"></textarea>
|
||||
</div>
|
||||
<!-- a 係數倍率微調 -->
|
||||
@@ -319,49 +330,76 @@
|
||||
</div>
|
||||
<template v-for="i in 6" :key="'as'+i">
|
||||
<div v-if="baseA[i] !== 0">
|
||||
<label class="text-xs text-slate-400 dark:text-gray-500 block mt-1">a{{ i }} (基準: {{
|
||||
baseA[i].toPrecision(6) }})</label>
|
||||
<input type="range" v-model.number="aSliders[i]" @input="updateFromSliders"
|
||||
<label class="text-xs text-slate-400 dark:text-gray-500 flex justify-between mt-1">
|
||||
<span>a{{ i }} (基準: {{ baseA[i].toPrecision(6) }})</span>
|
||||
<span v-if="outOfRangeA[i]" class="text-amber-500 font-bold animate-pulse">⚠️ 超出滑桿範圍</span>
|
||||
</label>
|
||||
<input type="range" v-model.number="aSliders[i]" @input="updateFromControls"
|
||||
:min="-maxLogA" :max="maxLogA" :step="maxLogA/100" class="w-full accent-orange-500">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</details>
|
||||
<!-- 定點數轉換 -->
|
||||
<div class="mt-5 pt-5 border-t border-slate-200 dark:border-gray-800">
|
||||
<h3 class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
|
||||
</section>
|
||||
|
||||
<hr class="border-slate-200 dark:border-gray-800">
|
||||
|
||||
<!-- 定點數轉換 -->
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>定點數係數轉換</span>
|
||||
<span class="text-xs bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded">Q{{ shiftBits }}</span>
|
||||
</h3>
|
||||
<div class="mb-3">
|
||||
<label class="text-xs text-slate-500 dark:text-gray-400 mb-1 block">左移位元數 (Shift Bits: 0~14)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" v-model.number="shiftBits" min="0" max="14" class="w-20 bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-2 text-sm outline-none text-slate-900 dark:text-gray-100">
|
||||
<span class="text-[10px] text-slate-400">Scaling: 2^{{ shiftBits }} ({{ Math.pow(2, shiftBits) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-[10px] text-slate-500 dark:text-gray-400 mb-1 block uppercase font-bold tracking-tighter">b (Integers)</label>
|
||||
<div class="bg-slate-100 dark:bg-gray-900/80 p-2 rounded-lg border border-slate-200 dark:border-gray-800 shadow-inner flex flex-wrap gap-2">
|
||||
<div v-for="item in fixedPointCoeffs.b" :key="item.label" class="bg-blue-50 dark:bg-blue-900/20 px-2 py-1 rounded border border-blue-100 dark:border-blue-800/30 flex gap-1">
|
||||
<span class="text-[10px] text-blue-400 font-bold">{{ item.label }}:</span>
|
||||
<span class="text-xs font-mono text-blue-700 dark:text-blue-300">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] text-slate-500 dark:text-gray-400 mb-1 block uppercase font-bold tracking-tighter">a (Integers)</label>
|
||||
<div class="bg-slate-100 dark:bg-gray-900/80 p-2 rounded-lg border border-slate-200 dark:border-gray-800 shadow-inner flex flex-wrap gap-2">
|
||||
<div v-for="item in fixedPointCoeffs.a" :key="item.label" class="bg-orange-50 dark:bg-orange-900/20 px-2 py-1 rounded border border-orange-100 dark:border-orange-800/30 flex gap-1">
|
||||
<span class="text-[10px] text-orange-400 font-bold">{{ item.label }}:</span>
|
||||
<span class="text-xs font-mono text-orange-700 dark:text-orange-300">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="Object.keys(fixedOverrides.a).length > 0 || Object.keys(fixedOverrides.b).length > 0"
|
||||
@click="clearFixedOverrides"
|
||||
class="text-[10px] font-bold bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 px-2 py-1 rounded text-slate-500 transition-colors uppercase mr-1">重設</button>
|
||||
<button @click="updateBodePlot"
|
||||
class="text-[10px] font-bold bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1 rounded transition-colors uppercase shadow-sm">套用</button>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 dark:text-gray-500 mt-2 italic">註:此數值為原始係數乘以 2^{{ shiftBits }} 後四捨五入之結果,可用於定點數 DSP 實作。</p>
|
||||
</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">左移位元數 (Shift Bits: 0~14)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" v-model.number="shiftBits" min="0" max="14" @change="clearFixedOverrides"
|
||||
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Scaling: 2^{{ shiftBits }} ({{ Math.pow(2, shiftBits) }})</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">定點分子係數 b (Integers)</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<div v-for="(item, idx) in fixedPointCoeffs.b" :key="item.label"
|
||||
:class="item.isOverridden ? 'border-blue-400 ring-1 ring-blue-400/20' : 'border-blue-100 dark:border-blue-800/30'"
|
||||
class="bg-blue-50 dark:bg-blue-900/20 px-1.5 py-0.5 rounded border flex items-center gap-1 transition-all">
|
||||
<span class="text-[9px] text-blue-400 font-bold">{{ item.label }}:</span>
|
||||
<span class="text-[10px] font-mono text-blue-700 dark:text-blue-300 font-bold"
|
||||
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea v-model="b_int_str" rows="2"
|
||||
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-blue-700 dark:text-blue-300"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">定點分母係數 a (Integers)</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<div v-for="(item, idx) in fixedPointCoeffs.a" :key="item.label"
|
||||
:class="item.isOverridden ? 'border-orange-400 ring-1 ring-orange-400/20' : 'border-orange-100 dark:border-orange-800/30'"
|
||||
class="bg-orange-50 dark:bg-orange-900/20 px-1.5 py-0.5 rounded border flex items-center gap-1 transition-all">
|
||||
<span class="text-[9px] text-orange-400 font-bold">{{ item.label }}:</span>
|
||||
<span class="text-[10px] font-mono text-orange-700 dark:text-orange-300 font-bold"
|
||||
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea v-model="a_int_str" rows="2"
|
||||
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-orange-700 dark:text-orange-300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 dark:text-gray-500 mt-3 italic">註:此數值為原始係數乘以 2^{{ shiftBits }} 後四捨五入之結果,可用於定點數 DSP 實作。</p>
|
||||
</section>
|
||||
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user