feat: implement A-coefficient normalization

This commit is contained in:
ws50529
2026-05-08 17:31:46 +08:00
parent 0ee69c4a49
commit a45569c94c
3 changed files with 214 additions and 59 deletions
+15 -7
View File
@@ -109,8 +109,14 @@ def design_filter(params: DesignParams):
a_s = [1.0, params.sogi_k * w0, w0**2]
b_new, a_new = signal.bilinear(b_s, a_s, fs=fs_val)
return {"b": b_new.tolist() if isinstance(b_new, np.ndarray) else b_new,
"a": a_new.tolist() if isinstance(a_new, np.ndarray) else a_new}
# 強制進行 a[0] 正規化
a_arr = np.array(a_new)
b_arr = np.array(b_new)
if a_arr[0] != 0 and a_arr[0] != 1.0:
b_arr = b_arr / a_arr[0]
a_arr = a_arr / a_arr[0]
return {"b": b_arr.tolist(), "a": a_arr.tolist()}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -118,7 +124,7 @@ def design_filter(params: DesignParams):
def calculate_bode(params: BodeParams):
try:
f_min = params.fs / 50000.0
f_max = params.fs * 3.162
f_max = params.fs # 數位濾波器通常顯示至 fs 或 Nyquist (fs/2) 即可
f_eval = np.logspace(np.log10(f_min), np.log10(f_max), 500)
# WorN determines frequencies to evaluate at
@@ -143,8 +149,9 @@ async def filter_csv(
col_idx: int = Form(0)
):
try:
b_vals = [float(x.strip()) for x in b.split(',') if x.strip()]
a_vals = [float(x.strip()) for x in a.split(',') if x.strip()]
# 支援逗號、空格等多種分隔符號
b_vals = [float(x.strip()) for x in b.replace(',', ' ').split() if x.strip()]
a_vals = [float(x.strip()) for x in a.replace(',', ' ').split() if x.strip()]
contents = await file.read()
df = pd.read_csv(io.BytesIO(contents))
@@ -178,8 +185,9 @@ async def filter_csv_download(
):
# 此端點專為產生包含輸出結果的 CSV 檔供下載
try:
b_vals = [float(x.strip()) for x in b.split(',') if x.strip()]
a_vals = [float(x.strip()) for x in a.split(',') if x.strip()]
# 支援逗號、空格等多種分隔符號
b_vals = [float(x.strip()) for x in b.replace(',', ' ').split() if x.strip()]
a_vals = [float(x.strip()) for x in a.replace(',', ' ').split() if x.strip()]
contents = await file.read()
df = pd.read_csv(io.BytesIO(contents))
+73 -19
View File
@@ -28,7 +28,7 @@ createApp({
baseB: [0.5, 0.5, 0, 0, 0, 0, 0],
baseA: [1, 0, 0, 0, 0, 0, 0],
bSliders: [0, 0, 0, 0, 0, 0, 0],
aSliders: [0, 0, 0, 0, 0, 0], // a1~a6 (index 0 unused for display but maps to a[1]~a[6])
aSliders: [0, 0, 0, 0, 0, 0, 0], // a1~a6 (index 0 unused for display but maps to a[1]~a[6])
sense_b: '2x', sense_a: '2x',
loadingBode: false, bodeTimeout: null, globalError: null,
@@ -39,6 +39,9 @@ createApp({
fixedOverrides: { a: {}, b: {} },
outOfRangeB: [false, false, false, false, false, false, false],
outOfRangeA: [false, false, false, false, false, false, false],
a1a2Diff: 0.0,
a1a2Sum: 0.0,
showAdvancedAdjustment: false,
}
},
@@ -121,6 +124,24 @@ createApp({
if (Math.abs(aVal - aTarget) > epsA) return true;
}
return false;
},
previewA1() {
const S = parseFloat(this.a1a2Sum || 0);
const D = parseFloat(this.a1a2Diff || 0);
return Math.round(((S + D) / 2) * 1000) / 1000;
},
previewA2() {
const S = parseFloat(this.a1a2Sum || 0);
const D = parseFloat(this.a1a2Diff || 0);
return Math.round(((S - D) / 2) * 1000) / 1000;
},
currentA1() {
const a = this.parseCoeffs(this.a_str);
return Math.round((a[1] || 0) * 1000) / 1000;
},
currentA2() {
const a = this.parseCoeffs(this.a_str);
return Math.round((a[2] || 0) * 1000) / 1000;
}
},
mounted() {
@@ -155,7 +176,9 @@ createApp({
return "";
},
parseCoeffs(str) {
return str.split(',').map(x => parseFloat(x.trim())).filter(x => !isNaN(x));
if (!str) return [];
// 支援逗號、空格、換行等分隔符號
return str.split(/[,\s]+/).map(x => parseFloat(x.trim())).filter(x => !isNaN(x));
},
padTo7(arr) {
const r = arr.slice(0, 7);
@@ -169,7 +192,7 @@ createApp({
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.aSliders = [0, 0, 0, 0, 0, 0, 0];
this.outOfRangeB = [false, false, false, false, false, false, false];
this.outOfRangeA = [false, false, false, false, false, false, false];
},
@@ -183,10 +206,13 @@ createApp({
},
clearManualAdjustments() {
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
this.aSliders = [0, 0, 0, 0, 0, 0];
this.aSliders = [0, 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(', ');
this.showAdvancedAdjustment = false;
this.calcSlidersFromText();
this.updateBodePlot();
},
updateFromControls() {
const currentB = this.baseB.map((base, i) => base * (this.systemGain || 1.0) * Math.pow(10, this.bSliders[i] || 0));
@@ -202,18 +228,36 @@ createApp({
// 只更新圖表,讓滑桿維持在相對應的偏差位置
this.updateBodePlot();
},
updateA1A2FromRelationship() {
const a = this.padTo7(this.parseCoeffs(this.a_str));
const S = parseFloat(this.a1a2Sum || 0);
const D = parseFloat(this.a1a2Diff || 0);
// a1 = (S + D) / 2, a2 = (S - D) / 2
a[1] = Math.round(((S + D) / 2) * 1000) / 1000;
a[2] = Math.round(((S - D) / 2) * 1000) / 1000;
this.a_str = a.map(x => parseFloat(x.toPrecision(10))).join(', ');
// 同步其他滑桿位置
this.calcSlidersFromText();
},
enableAdvancedAdjustment() {
this.calcSlidersFromText();
this.showAdvancedAdjustment = true;
},
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 (base !== 0 && val !== 0 && (val / (base * gain)) > 0) {
let logVal = Math.log10(val / (base * gain));
if (Math.abs(logVal) < 1e-12) logVal = 0; // 消除浮點數微小誤差
if (Math.abs(logVal) > this.maxLogB) this.outOfRangeB[i] = true;
this.bSliders[i] = Math.max(-this.maxLogB, Math.min(this.maxLogB, logVal));
} else {
@@ -226,8 +270,9 @@ createApp({
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 (base !== 0 && val !== 0 && (val / base) > 0) {
let logVal = Math.log10(val / base);
if (Math.abs(logVal) < 1e-12) logVal = 0; // 消除浮點數微小誤差
if (Math.abs(logVal) > this.maxLogA) this.outOfRangeA[i] = true;
this.aSliders[i] = Math.max(-this.maxLogA, Math.min(this.maxLogA, logVal));
} else {
@@ -235,11 +280,16 @@ createApp({
if (val !== 0 && base !== 0) this.outOfRangeA[i] = true;
}
}
// 同步更新 a1a2Diff 與 a1a2Sum 顯示值
const ra1 = Math.round((currentA[1] || 0) * 1000) / 1000;
const ra2 = Math.round((currentA[2] || 0) * 1000) / 1000;
this.a1a2Diff = ra1 - ra2;
this.a1a2Sum = ra1 + ra2;
},
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();
},
@@ -247,7 +297,7 @@ createApp({
// 允許的控制鍵
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'];
if (allowedKeys.includes(e.key) || e.ctrlKey || e.metaKey) return;
// 檢查字元是否合法
const charRegex = allowDot ? /[0-9,.\-\s]/ : /[0-9,\-\s]/;
if (!charRegex.test(e.key)) {
@@ -306,7 +356,7 @@ createApp({
this.systemGain = 1.0;
this.fixedOverrides = { a: {}, b: {} };
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
this.aSliders = [0, 0, 0, 0, 0, 0];
this.aSliders = [0, 0, 0, 0, 0, 0, 0];
this.outOfRangeB = [false, false, false, false, false, false, false];
this.outOfRangeA = [false, false, false, false, false, false, false];
@@ -353,12 +403,13 @@ createApp({
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.aSliders = [0, 0, 0, 0, 0, 0, 0];
this.showAdvancedAdjustment = false;
this.updateFromControls();
// 3. 更新圖表
this.updateBodePlot();
} catch (e) { this.globalError = e.message; }
@@ -394,7 +445,7 @@ createApp({
]);
if (!resIdeal.ok || !resFixed.ok) throw new Error('無法計算頻率響應');
const dataIdeal = await resIdeal.json();
const dataFixed = await resFixed.json();
@@ -488,7 +539,7 @@ createApp({
annotations: [
{ text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.15, 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.15, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
// 手動 Magnitude 圖例 (位於上方圖表下方)
{ xref: 'paper', yref: 'paper', x: 0.35, y: isSmallScreen ? 0.52 : 0.54, text: '一一一', font: { color: isDark ? '#3b82f6' : '#2563eb', size: 10 }, showarrow: false },
{ xref: 'paper', yref: 'paper', x: 0.42, y: isSmallScreen ? 0.52 : 0.54, text: 'Ideal (Float)', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' },
@@ -512,7 +563,7 @@ createApp({
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' };
const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2' };
@@ -562,8 +613,11 @@ createApp({
if (!res.ok) { const err = await res.json(); throw new Error(err.detail || '濾波處理失敗'); }
const data = await res.json();
this.timePlotData = data;
this.drawTimePlot(data);
this.filterDone = true;
// 確保在可見狀態下繪圖,避免 Plotly 計算尺寸錯誤
this.$nextTick(() => {
this.drawTimePlot(data);
});
} catch (e) { this.globalError = e.message; }
finally { this.loadingFilter = false; }
},
+126 -33
View File
@@ -231,15 +231,17 @@
系統參數</h2>
<div class="space-y-3">
<div>
<label class="text-sm text-slate-500 dark:text-gray-400 flex justify-between mb-1"><span>取樣頻率 fs
<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" @input="debouncedApply"
@keydown="onlyNumberKey"
<input type="number" v-model.number="fs" @input="debouncedApply" @keydown="onlyNumberKey"
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>系統增益 K (System Gain)</span><span class="text-blue-600 dark:text-blue-400">{{ systemGain }}x</span></label>
<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"
@keydown="onlyNumberKey"
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">
@@ -255,8 +257,7 @@
class="text-sm font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
<span>濾波器係數設定</span>
<div class="flex items-center gap-2">
<button v-if="isManualDirty"
@click="clearManualAdjustments"
<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>
@@ -266,12 +267,15 @@
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">前饋分子係數 b (b0 ~ b6)</label>
<!-- b 係數標記顯示 -->
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="item in floatingCoeffs.b" :key="item.label" class="bg-blue-50 dark:bg-blue-900/30 px-1.5 py-0.5 rounded border border-blue-100 dark:border-blue-800/20 flex gap-1 items-center">
<div v-for="item in floatingCoeffs.b" :key="item.label"
class="bg-blue-50 dark:bg-blue-900/30 px-1.5 py-0.5 rounded border border-blue-100 dark:border-blue-800/20 flex gap-1 items-center">
<span class="text-[9px] text-blue-500 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-blue-800 dark:text-blue-300">{{ item.val }}</span>
<span class="text-[10px] font-mono text-blue-800 dark:text-blue-300">{{ item.val
}}</span>
</div>
</div>
<textarea v-model="b_str" @input="debouncedUpdateFromText('b')" @keydown="onlyNumberKey" rows="2"
<textarea v-model="b_str" @input="debouncedUpdateFromText('b')" @keydown="onlyNumberKey"
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 係數倍率微調 -->
@@ -293,25 +297,32 @@
<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 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>
<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">
:min="-maxLogB" :max="maxLogB" :step="maxLogB/100"
class="w-full accent-blue-500">
</div>
</template>
</div>
</details>
<div class="mb-3">
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">回饋分母係數 a (a0=1, a1 ~ a6)</label>
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">回饋分母係數 a (a0=1, a1 ~
a6)</label>
<!-- a 係數標記顯示 -->
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="item in floatingCoeffs.a" :key="item.label" class="bg-orange-50 dark:bg-orange-900/30 px-1.5 py-0.5 rounded border border-orange-100 dark:border-orange-800/20 flex gap-1 items-center">
<div v-for="item in floatingCoeffs.a" :key="item.label"
class="bg-orange-50 dark:bg-orange-900/30 px-1.5 py-0.5 rounded border border-orange-100 dark:border-orange-800/20 flex gap-1 items-center">
<span class="text-[9px] text-orange-500 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-orange-800 dark:text-orange-300">{{ item.val }}</span>
<span class="text-[10px] font-mono text-orange-800 dark:text-orange-300">{{ item.val
}}</span>
</div>
</div>
<textarea v-model="a_str" @input="debouncedUpdateFromText('a')" @keydown="onlyNumberKey" rows="2"
<textarea v-model="a_str" @input="debouncedUpdateFromText('a')" @keydown="onlyNumberKey"
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 係數倍率微調 -->
@@ -333,28 +344,103 @@
<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 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>
<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">
:min="-maxLogA" :max="maxLogA" :step="maxLogA/100"
class="w-full accent-orange-500">
</div>
</template>
</div>
</details>
<!-- 進階係數關聯調整 (X = a1 - a2) -->
<div class="mt-4">
<button v-if="!showAdvancedAdjustment" @click="enableAdvancedAdjustment"
class="w-full py-3 border-2 border-dashed border-slate-200 dark:border-gray-800 rounded-xl text-slate-400 dark:text-gray-500 text-xs font-bold hover:bg-slate-50 dark:hover:bg-gray-800/50 hover:border-indigo-300 dark:hover:border-indigo-900 transition-all flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
</svg>
開啟進階係數關聯調整 (a1, a2)
</button>
<div v-if="showAdvancedAdjustment" class="p-4 bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-xl shadow-inner">
<h3 class="text-xs font-bold text-slate-500 dark:text-gray-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
進階係數關聯調整
</h3>
<!-- 差距調整 (X) -->
<div class="mb-4">
<label class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 flex justify-between mb-2">
<span>差距 (a1 - a2)</span>
<span class="text-indigo-600 dark:text-indigo-400 font-mono bg-indigo-50 dark:bg-indigo-900/30 px-2 py-0.5 rounded">{{ a1a2Diff.toFixed(3) }}</span>
</label>
<input type="range" v-model.number="a1a2Diff" min="-4" max="4" step="0.001"
@input="updateA1A2FromRelationship"
class="w-full accent-indigo-500 h-1.5 bg-slate-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
<!-- 平移調整 (S) -->
<div class="mb-4">
<label class="text-[11px] font-semibold text-slate-400 dark:text-gray-500 flex justify-between mb-2">
<span>平移 (a1 + a2)</span>
<span class="text-emerald-600 dark:text-emerald-400 font-mono bg-emerald-50 dark:bg-emerald-900/30 px-2 py-0.5 rounded">{{ a1a2Sum.toFixed(3) }}</span>
</label>
<input type="range" v-model.number="a1a2Sum" min="-4" max="4" step="0.001"
@input="updateA1A2FromRelationship"
class="w-full accent-emerald-500 h-1.5 bg-slate-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
<!-- 預覽結果 -->
<div class="flex gap-2 mb-4">
<div class="flex-1 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a1 變化預覽</span>
<div class="flex items-center justify-center gap-1.5">
<span class="text-[10px] font-mono text-slate-400">{{ currentA1.toFixed(3) }}</span>
<svg class="w-2.5 h-2.5 text-slate-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
<span class="text-xs font-mono font-bold text-indigo-600 dark:text-indigo-400">{{ previewA1.toFixed(3) }}</span>
</div>
</div>
<div class="flex-1 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-400 block uppercase font-bold mb-1">a2 變化預覽</span>
<div class="flex items-center justify-center gap-1.5">
<span class="text-[10px] font-mono text-slate-400">{{ currentA2.toFixed(3) }}</span>
<svg class="w-2.5 h-2.5 text-slate-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
<span class="text-xs font-mono font-bold text-emerald-600 dark:text-emerald-400">{{ previewA2.toFixed(3) }}</span>
</div>
</div>
</div>
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-800/30 rounded-lg p-3">
<p class="text-[10px] text-amber-700 dark:text-amber-400 leading-relaxed">
<span class="font-bold">運算說明:</span><br>
調整時,系統會先將現有係數四捨五入至三位小數再計算基準值。調整結果會自動將 a1, a2 存回為三位小數精度。
</p>
</div>
</div>
</div>
</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">
<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>
<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>
</div>
<div class="flex items-center gap-2">
<button v-if="Object.keys(fixedOverrides.a).length > 0 || Object.keys(fixedOverrides.b).length > 0"
<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"
@@ -363,19 +449,23 @@
</h2>
<div class="mb-4">
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">左移位元數 (Shift Bits: 0~14)</label>
<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" @keydown="onlyNumberKey"
<input type="number" v-model.number="shiftBits" min="0" max="14"
@change="clearFixedOverrides" @keydown="onlyNumberKey"
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>
<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>
<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"
<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>
@@ -387,9 +477,10 @@
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>
<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"
<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>
@@ -401,7 +492,8 @@
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>
<p class="text-[10px] text-slate-400 dark:text-gray-500 mt-3 italic">註:此數值為原始係數乘以 2^{{ shiftBits }}
後四捨五入之結果,可用於定點數 DSP 實作。</p>
</section>
</aside>
@@ -423,7 +515,8 @@
<!-- Bode Plot -->
<div
class="bg-white dark:bg-dark lg:rounded-xl border-b lg:border border-slate-100 lg:border-slate-200 dark:border-gray-800 lg:dark:border-gray-800 lg:shadow-md p-0 lg:p-1 flex-shrink-0 relative transition-colors duration-300">
<div class="hidden lg:block absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 pointer-events-none">
<div
class="hidden lg:block absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 pointer-events-none">
</div>
<div
class="px-5 py-4 border-b border-slate-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-dark backdrop-blur-sm z-10 relative">