feat: 優化時域圖表X軸顯示時間單位,並新增步階訊號測試腳本
This commit is contained in:
+17
-2
@@ -123,8 +123,23 @@
|
|||||||
- **工程對齊**:徹底落實 `INTEGER_FILTER_IMPLEMENTATION.md` 第 7 節強調的「二階級聯黃金法則」,避免了直接將高階濾波器展開成 Direct Form 所帶來的極點靈敏度暴增及數值不穩定問題。
|
- **工程對齊**:徹底落實 `INTEGER_FILTER_IMPLEMENTATION.md` 第 7 節強調的「二階級聯黃金法則」,避免了直接將高階濾波器展開成 Direct Form 所帶來的極點靈敏度暴增及數值不穩定問題。
|
||||||
|
|
||||||
## 13. 目前 Git 狀態與 GitLab 同步
|
## 13. 目前 Git 狀態與 GitLab 同步
|
||||||
- **當前分支**:`cascadedfilters`
|
- **當前分支**:`cascadedfilters` (基於 `dev-v2-integretimedomain` 建立)
|
||||||
- **提交與推送**:已將多級串聯 (Cascade) 的完整實作獨立建立為 `cascadedfilters` 分支,並推送至 GitLab 以供後續檢視與整合。
|
- **分支策略**:為了繼承並保留「定點數時域模擬」的核心邏輯,特別選擇從 `dev-v2-integretimedomain` 岔出建立新分支,而非從 `main` 分支。這樣確保了時域整數運算功能與多級串聯功能的完美結合。
|
||||||
|
- **提交與推送**:已將多級串聯 (Cascade) 的完整實作獨立建立為 `cascadedfilters` 分支,並推送至 GitLab 以供同事試用與後續整合。
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last Updated: 2026-05-21 14:19*
|
*Last Updated: 2026-05-21 14:19*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DSP Bode Plot 專案進度更新 (2026-05-22 - UI/UX 微調與測試工具)
|
||||||
|
|
||||||
|
## 14. 時域繪圖 (Time Plot) X 軸真實時間單位
|
||||||
|
- **直覺化顯示**:將原本時域繪圖橫軸的「Sample Index (取樣點索引)」替換為「真實時間 (Time)」。
|
||||||
|
- **動態單位切換**:系統利用已知的取樣率 (`fs`),會根據波形總時間長度自動選擇最適合的顯示單位(包含秒 `s`、毫秒 `ms`、以及微秒 `μs`),使波形觀察更符合硬體工程師的直覺。
|
||||||
|
- **全面升級**:此改動已同步套用於新版 Vue.js 介面 (`src/app-options.js`) 以及舊版 Streamlit 介面 (`dea_legacy.py`)。
|
||||||
|
|
||||||
|
## 15. 測試訊號產生器擴充
|
||||||
|
- **步階訊號腳本**:新增了 `generate_step_signal.py`,支援自動產生精確時間長度 (預設 30 ms) 與多種動態範圍 (0 to 1, 0.1 to 0.9, 0.2 to 0.8 等) 的步階函數測試 CSV 檔,方便快速驗證濾波器的步階響應 (Step Response) 與穩定時間 (Settling Time)。
|
||||||
|
|
||||||
|
*Last Updated: 2026-05-22 06:15*
|
||||||
|
|||||||
+499000
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+13
-2
@@ -649,14 +649,25 @@ if uploaded_file is not None:
|
|||||||
st.subheader("波形比較圖")
|
st.subheader("波形比較圖")
|
||||||
fig_time = go.Figure()
|
fig_time = go.Figure()
|
||||||
|
|
||||||
x_axis = df.index
|
max_time_s = len(df.index) / fs_val
|
||||||
|
if max_time_s > 0 and max_time_s < 1e-3:
|
||||||
|
time_mult = 1e6
|
||||||
|
time_unit = "μs"
|
||||||
|
elif max_time_s > 0 and max_time_s < 1:
|
||||||
|
time_mult = 1e3
|
||||||
|
time_unit = "ms"
|
||||||
|
else:
|
||||||
|
time_mult = 1.0
|
||||||
|
time_unit = "s"
|
||||||
|
|
||||||
|
x_axis = (df.index / fs_val) * time_mult
|
||||||
|
|
||||||
fig_time.add_trace(go.Scatter(x=x_axis, y=x_signal, name='原始輸入訊號 (Input)', opacity=0.7, line=dict(color='#00cc96')))
|
fig_time.add_trace(go.Scatter(x=x_axis, y=x_signal, name='原始輸入訊號 (Input)', opacity=0.7, line=dict(color='#00cc96')))
|
||||||
fig_time.add_trace(go.Scatter(x=x_axis, y=y_signal, name='濾波後輸出 (Output)', opacity=0.9, line=dict(color='#ef553b')))
|
fig_time.add_trace(go.Scatter(x=x_axis, y=y_signal, name='濾波後輸出 (Output)', opacity=0.9, line=dict(color='#ef553b')))
|
||||||
|
|
||||||
fig_time.update_layout(
|
fig_time.update_layout(
|
||||||
height=500,
|
height=500,
|
||||||
xaxis_title="Sample Index",
|
xaxis_title=f"Time ({time_unit})",
|
||||||
yaxis_title="Amplitude",
|
yaxis_title="Amplitude",
|
||||||
template="plotly_dark",
|
template="plotly_dark",
|
||||||
legend=dict(
|
legend=dict(
|
||||||
|
|||||||
+64
-26
@@ -1,26 +1,64 @@
|
|||||||
INFO: Started server process [13028]
|
INFO: 127.0.0.1:42278 - "GET /ui/ HTTP/1.1" 200 OK
|
||||||
INFO: Waiting for application startup.
|
INFO: 127.0.0.1:42278 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
INFO: Application startup complete.
|
INFO: 127.0.0.1:42278 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: Uvicorn running on https://0.0.0.0:8000 (Press CTRL+C to quit)
|
INFO: 127.0.0.1:42278 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "GET /ui/ HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:42278 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "POST /api/design HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:42278 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39516 - "POST /api/csv/upload HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39516 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39526 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53686 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39526 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "GET / HTTP/1.1" 307 Temporary Redirect
|
INFO: 127.0.0.1:39526 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "GET /ui/ HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39526 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "GET /ui/build/app.css?v=cascade_v1 HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39526 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "GET /ui/build/app.js?v=cascade_v1 HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:39526 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "POST /api/design HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:50436 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "GET /ui/ HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "GET /ui/build/app.css?v=cascade_v1 HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:53694 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "GET /ui/build/app.js?v=cascade_v1 HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:42946 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:42946 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:40394 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:40394 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:45822 - "GET /ui/logo.png HTTP/1.1" 200 OK
|
||||||
INFO: Shutting down
|
INFO: 127.0.0.1:45822 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
INFO: Waiting for application shutdown.
|
INFO: 127.0.0.1:45828 - "POST /api/csv/upload HTTP/1.1" 200 OK
|
||||||
INFO: Application shutdown complete.
|
INFO: 127.0.0.1:45828 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
INFO: Finished server process [13028]
|
INFO: 127.0.0.1:49574 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49576 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49576 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49574 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58970 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58970 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58970 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58970 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58970 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58976 - "POST /api/design HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58976 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58976 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:58976 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:54780 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49440 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49454 - "POST /api/bode/compare_cascade HTTP/1.1" 200 OK
|
||||||
|
INFO: 127.0.0.1:49454 - "POST /api/filter HTTP/1.1" 200 OK
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
fs = 100000 # 100 kHz
|
||||||
|
t_total = 0.030 # 30 ms
|
||||||
|
# 建立時間陣列:0 ~ 30ms
|
||||||
|
t = np.arange(0, t_total, 1/fs)
|
||||||
|
|
||||||
|
step_mask = t >= 0.001 # 在 1 ms 時觸發 step 改變
|
||||||
|
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'time': np.round(t, 6), # 修正浮點數微小誤差
|
||||||
|
'step_0_to_1': np.where(step_mask, 1.0, 0.0),
|
||||||
|
'step_0.1_to_0.9': np.where(step_mask, 0.9, 0.1),
|
||||||
|
'step_0.2_to_0.8': np.where(step_mask, 0.8, 0.2),
|
||||||
|
'step_1_to_0': np.where(step_mask, 0.0, 1.0),
|
||||||
|
'step_0.9_to_0.1': np.where(step_mask, 0.1, 0.9),
|
||||||
|
'step_0.8_to_0.2': np.where(step_mask, 0.2, 0.8)
|
||||||
|
})
|
||||||
|
|
||||||
|
output_path = os.path.join(os.path.dirname(__file__), 'step_signal.csv')
|
||||||
|
df.to_csv(output_path, index=False)
|
||||||
|
print(f"成功建立 {output_path},包含 {len(df)} 筆資料點。")
|
||||||
+17
-4
@@ -1292,18 +1292,31 @@ export default {
|
|||||||
const textColor = isDark ? '#b4b4bd' : '#525663';
|
const textColor = isDark ? '#b4b4bd' : '#525663';
|
||||||
const plotBgColor = isDark ? '#090a0d' : '#ffffff';
|
const plotBgColor = isDark ? '#090a0d' : '#ffffff';
|
||||||
|
|
||||||
|
const maxTimeS = (data.index.length > 0 ? Math.max(...data.index) : 0) / this.fs;
|
||||||
|
let timeMultiplier = 1;
|
||||||
|
let timeUnit = 's';
|
||||||
|
if (maxTimeS > 0 && maxTimeS < 1e-3) {
|
||||||
|
timeMultiplier = 1e6;
|
||||||
|
timeUnit = 'μs';
|
||||||
|
} else if (maxTimeS > 0 && maxTimeS < 1) {
|
||||||
|
timeMultiplier = 1e3;
|
||||||
|
timeUnit = 'ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
const xData = data.index.map(i => (i / this.fs) * timeMultiplier);
|
||||||
|
|
||||||
const layout = {
|
const layout = {
|
||||||
paper_bgcolor: plotBgColor, plot_bgcolor: plotBgColor,
|
paper_bgcolor: plotBgColor, plot_bgcolor: plotBgColor,
|
||||||
margin: isSmallScreen ? { t: 60, b: 60, l: 45, r: 15 } : { t: 40, b: 55, l: 60, r: 20 },
|
margin: isSmallScreen ? { t: 60, b: 60, l: 45, r: 15 } : { t: 40, b: 55, l: 60, r: 20 },
|
||||||
font: { color: textColor, family: 'Google Sans, Roboto, sans-serif' },
|
font: { color: textColor, family: 'Google Sans, Roboto, sans-serif' },
|
||||||
xaxis: { title: { text: 'Sample Index', font: { size: isSmallScreen ? 10 : 11 } }, gridcolor: gridColor, zerolinecolor: zeroLineColor, tickfont: { color: textColor, size: isSmallScreen ? 9 : 11 } },
|
xaxis: { title: { text: `Time (${timeUnit})`, font: { size: isSmallScreen ? 10 : 11 } }, gridcolor: gridColor, zerolinecolor: zeroLineColor, tickfont: { color: textColor, size: isSmallScreen ? 9 : 11 } },
|
||||||
yaxis: { title: { text: 'Amplitude', font: { size: isSmallScreen ? 10 : 11 } }, gridcolor: gridColor, zerolinecolor: zeroLineColor, tickfont: { color: textColor, size: isSmallScreen ? 9 : 11 } },
|
yaxis: { title: { text: 'Amplitude', font: { size: isSmallScreen ? 10 : 11 } }, gridcolor: gridColor, zerolinecolor: zeroLineColor, tickfont: { color: textColor, size: isSmallScreen ? 9 : 11 } },
|
||||||
legend: { orientation: 'h', y: 1.02, yanchor: 'bottom', x: 1, xanchor: 'right', font: { color: textColor, size: isSmallScreen ? 9 : 10 } }
|
legend: { orientation: 'h', y: 1.02, yanchor: 'bottom', x: 1, xanchor: 'right', font: { color: textColor, size: isSmallScreen ? 9 : 10 } }
|
||||||
};
|
};
|
||||||
Plotly.react('timePlot', [
|
Plotly.react('timePlot', [
|
||||||
{ x: data.index, y: data.original, name: '原始輸入 (Input)', type: 'scatter', line: { color: isDark ? '#c8cee0' : '#566075', width: 2.4 }, opacity: 0.5 },
|
{ x: xData, y: data.original, name: '原始輸入 (Input)', type: 'scatter', line: { color: isDark ? '#c8cee0' : '#566075', width: 2.4 }, opacity: 0.5 },
|
||||||
{ x: data.index, y: data.filtered, name: '理想串聯路徑 (Cascade Ideal)', type: 'scatter', line: { color: isDark ? '#64d2ff' : '#2d6f8f', width: 2.8 }, opacity: 0.95 },
|
{ x: xData, y: data.filtered, name: '理想串聯路徑 (Cascade Ideal)', type: 'scatter', line: { color: isDark ? '#64d2ff' : '#2d6f8f', width: 2.8 }, opacity: 0.95 },
|
||||||
{ x: data.index, y: data.filtered_fixed, name: '定點串聯路徑 (Cascade Fixed)', type: 'scatter', line: { color: isDark ? '#ff7c7c' : '#c0392b', width: 2.8, dash: 'dot' }, opacity: 0.95 }
|
{ x: xData, y: data.filtered_fixed, name: '定點串聯路徑 (Cascade Fixed)', type: 'scatter', line: { color: isDark ? '#ff7c7c' : '#c0392b', width: 2.8, dash: 'dot' }, opacity: 0.95 }
|
||||||
], layout, { responsive: true });
|
], layout, { responsive: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+3001
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user