From a45569c94c1211813050ac26e9df5023cd81afe4 Mon Sep 17 00:00:00 2001 From: ws50529 Date: Fri, 8 May 2026 17:31:46 +0800 Subject: [PATCH] feat: implement A-coefficient normalization --- dea_api.py | 22 +++++-- static/app.js | 92 +++++++++++++++++++++------ static/index.html | 159 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 214 insertions(+), 59 deletions(-) diff --git a/dea_api.py b/dea_api.py index dd0b549..8806cfc 100755 --- a/dea_api.py +++ b/dea_api.py @@ -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)) diff --git a/static/app.js b/static/app.js index e6b386a..881f25a 100644 --- a/static/app.js +++ b/static/app.js @@ -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; } }, diff --git a/static/index.html b/static/index.html index feeb2ca..0df25f0 100644 --- a/static/index.html +++ b/static/index.html @@ -231,15 +231,17 @@ 系統參數
-
- + @@ -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"> 濾波器係數設定
- @@ -266,12 +267,15 @@
-
+
{{ item.label }}: - {{ item.val }} + {{ item.val + }}
-
@@ -293,25 +297,32 @@
- +
-
+
{{ item.label }}: - {{ item.val }} + {{ item.val + }}
-
@@ -333,28 +344,103 @@
+ + +
+ + +
+

+ + + + 進階係數關聯調整 +

+ + +
+ + +
+ + +
+ + +
+ + +
+
+ a1 變化預覽 +
+ {{ currentA1.toFixed(3) }} + + {{ previewA1.toFixed(3) }} +
+
+
+ a2 變化預覽 +
+ {{ currentA2.toFixed(3) }} + + {{ previewA2.toFixed(3) }} +
+
+
+ +
+

+ 運算說明:
+ 調整時,系統會先將現有係數四捨五入至三位小數再計算基準值。調整結果會自動將 a1, a2 存回為三位小數精度。 +

+
+
+

-

+

定點數係數轉換 - Q{{ shiftBits }} + Q{{ + shiftBits }}
-

@@ -423,7 +515,8 @@
-