feat: implement A-coefficient normalization
This commit is contained in:
+15
-7
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user