2026-05-14 09:41:00 +08:00
|
|
|
import io
|
2026-05-17 00:58:19 +08:00
|
|
|
import os
|
|
|
|
|
import uuid
|
|
|
|
|
import re
|
2026-05-14 09:41:00 +08:00
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pandas as pd
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
from scipy import signal
|
2026-05-17 00:58:19 +08:00
|
|
|
import math
|
2026-05-14 09:41:00 +08:00
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
from .config import MAX_CSV_BYTES, MAX_PLOT_POINTS, MAX_ROWS
|
2026-05-14 09:41:00 +08:00
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
TEMP_CSV_DIR = "temp_csv"
|
|
|
|
|
os.makedirs(TEMP_CSV_DIR, exist_ok=True)
|
2026-05-14 09:41:00 +08:00
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
def get_cached_file_path(file_id):
|
|
|
|
|
if not re.match(r'^[0-9a-f\-]{36}$', file_id):
|
|
|
|
|
raise HTTPException(status_code=400, detail="無效的檔案ID")
|
|
|
|
|
file_path = os.path.join(TEMP_CSV_DIR, f"{file_id}.csv")
|
|
|
|
|
if not os.path.exists(file_path):
|
|
|
|
|
raise HTTPException(status_code=404, detail="找不到快取的 CSV 檔案,請重新上傳")
|
|
|
|
|
return file_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def downsample_for_plot(index, original, filtered_float, filtered_fixed):
|
2026-05-14 09:41:00 +08:00
|
|
|
total = len(index)
|
|
|
|
|
if total <= MAX_PLOT_POINTS:
|
2026-05-17 00:58:19 +08:00
|
|
|
return index, original, filtered_float, filtered_fixed, 1
|
2026-05-14 09:41:00 +08:00
|
|
|
step = int(np.ceil(total / MAX_PLOT_POINTS))
|
2026-05-17 00:58:19 +08:00
|
|
|
return index[::step], original[::step], filtered_float[::step], filtered_fixed[::step], step
|
2026-05-14 09:41:00 +08:00
|
|
|
|
|
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
async def save_csv_upload(file):
|
2026-05-14 09:41:00 +08:00
|
|
|
filename = (file.filename or "").lower()
|
|
|
|
|
if filename and not filename.endswith(".csv"):
|
|
|
|
|
raise HTTPException(status_code=400, detail="請上傳 CSV 檔案")
|
|
|
|
|
contents = await file.read(MAX_CSV_BYTES + 1)
|
|
|
|
|
if len(contents) > MAX_CSV_BYTES:
|
|
|
|
|
raise HTTPException(status_code=413, detail=f"CSV 檔案不可超過 {MAX_CSV_BYTES // (1024 * 1024)}MB")
|
|
|
|
|
if not contents.strip():
|
|
|
|
|
raise HTTPException(status_code=400, detail="CSV 不可為空")
|
2026-05-17 00:58:19 +08:00
|
|
|
|
|
|
|
|
file_id = str(uuid.uuid4())
|
|
|
|
|
file_path = os.path.join(TEMP_CSV_DIR, f"{file_id}.csv")
|
|
|
|
|
with open(file_path, "wb") as f:
|
|
|
|
|
f.write(contents)
|
|
|
|
|
return file_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def integer_lfilter(b_int, a_int, x_float, shift_in, shift_out, shift_b, shift_a, use_round=False):
|
|
|
|
|
"""
|
|
|
|
|
全整數差分方程式模擬 (Mimic DSP hardware)
|
|
|
|
|
高精度狀態變數架構:前饋不位移,保留小數精度於狀態變數中。
|
|
|
|
|
支援硬體 Rounding (四捨五入) vs Floor (無條件捨去向下取整)。
|
|
|
|
|
"""
|
|
|
|
|
b_int = np.asarray(b_int, dtype=np.int64)
|
|
|
|
|
a_int = np.asarray(a_int, dtype=np.int64)
|
|
|
|
|
x_int = np.round(x_float * (2**shift_in)).astype(np.int64)
|
|
|
|
|
|
|
|
|
|
# y_hist 將保留在 Q_{in + b} 格式以降低 Truncation Error
|
|
|
|
|
y_hist = np.zeros(len(x_int), dtype=np.int64)
|
|
|
|
|
y_out = np.zeros(len(x_int), dtype=np.int64)
|
|
|
|
|
|
|
|
|
|
nb = len(b_int)
|
|
|
|
|
na = len(a_int)
|
|
|
|
|
|
|
|
|
|
A0 = int(a_int[0])
|
|
|
|
|
if A0 == 0: A0 = 1
|
|
|
|
|
|
|
|
|
|
# 輸出所需的總位移量
|
|
|
|
|
out_shift = shift_in + shift_b - shift_out
|
|
|
|
|
|
|
|
|
|
# 預先計算四捨五入的補償值 (+0.5)
|
|
|
|
|
# 韌體開發提示 (C Implementation / RISC-V):
|
|
|
|
|
# 1. 標準 RISC-V (RV32I/IMAC) 的 SRA 指令是純 Floor (無條件捨去),沒有硬體 Rounding shift。
|
|
|
|
|
# (除非具備 'P' DSP Extension 才可能有 1-cycle 的硬體 rounding shift)。
|
|
|
|
|
# 2. 演算法秘技:在 C 語言中要實現 1-clock 的四捨五入,不要呼叫 float 的 round()。
|
|
|
|
|
# 請使用 `y = (acc + (1 << (shift - 1))) >> shift`。
|
|
|
|
|
# 編譯器會將 (1 << (shift - 1)) 編譯為常數,整體只消耗 1 個 ADD 指令,極大消除了 DC Bias。
|
|
|
|
|
round_offset_a = (A0 >> 1) if use_round else 0
|
|
|
|
|
round_offset_out = (1 << (out_shift - 1)) if (use_round and out_shift > 0) else 0
|
|
|
|
|
|
|
|
|
|
for n in range(len(x_int)):
|
|
|
|
|
acc = 0
|
|
|
|
|
# Feedforward: 前饋完全不位移,結果為 Q_{in + b}
|
|
|
|
|
for i in range(nb):
|
|
|
|
|
if n - i >= 0:
|
|
|
|
|
acc += b_int[i] * x_int[n - i]
|
|
|
|
|
|
|
|
|
|
# Feedback: 歷史紀錄為 Q_{in + b},係數為 Q_a,乘積為 Q_{in + b + a}
|
|
|
|
|
fb_sum = 0
|
|
|
|
|
for j in range(1, na):
|
|
|
|
|
if n - j >= 0:
|
|
|
|
|
fb_sum += a_int[j] * y_hist[n - j]
|
|
|
|
|
|
|
|
|
|
# Feedback 縮放:除以 A0 (即 >> shift_a),結果退回 Q_{in + b} 完美對齊
|
|
|
|
|
# Python 的 // 等同於硬體的 SRA (Arithmetic Right Shift),會向負無窮大 Floor
|
|
|
|
|
fb_shifted = (fb_sum + round_offset_a) // A0
|
|
|
|
|
|
|
|
|
|
acc -= fb_shifted
|
|
|
|
|
|
|
|
|
|
# 將超高精度的 acc 直接存入歷史變數
|
|
|
|
|
y_hist[n] = acc
|
|
|
|
|
|
|
|
|
|
# 最終輸出再針對 Q_out 進行位移縮放
|
|
|
|
|
if out_shift > 0:
|
|
|
|
|
y_out[n] = (acc + round_offset_out) >> out_shift
|
|
|
|
|
elif out_shift < 0:
|
|
|
|
|
y_out[n] = acc << (-out_shift)
|
|
|
|
|
else:
|
|
|
|
|
y_out[n] = acc
|
|
|
|
|
|
|
|
|
|
return y_out.astype(float) / (2**shift_out)
|
2026-05-14 09:41:00 +08:00
|
|
|
|
|
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False):
|
|
|
|
|
path = get_cached_file_path(file_id)
|
|
|
|
|
|
|
|
|
|
# 預先讀取欄位名稱,避免用 usecols 讀取後找不到原始索引
|
|
|
|
|
cols = pd.read_csv(path, nrows=0).columns.tolist()
|
|
|
|
|
if col_idx < 0 or col_idx >= len(cols):
|
2026-05-14 09:41:00 +08:00
|
|
|
raise HTTPException(status_code=400, detail="欄位索引超出範圍")
|
2026-05-17 00:58:19 +08:00
|
|
|
col_to_filter = cols[col_idx]
|
|
|
|
|
|
|
|
|
|
# 精準讀取單一欄位,並加上筆數限制 (極大降低記憶體用量與時間)
|
|
|
|
|
df = pd.read_csv(path, usecols=[col_idx], nrows=MAX_ROWS)
|
2026-05-14 09:41:00 +08:00
|
|
|
x_signal = pd.to_numeric(df[col_to_filter], errors="coerce")
|
2026-05-17 00:58:19 +08:00
|
|
|
|
2026-05-14 09:41:00 +08:00
|
|
|
if x_signal.isna().any():
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非數值資料")
|
|
|
|
|
x_values = x_signal.to_numpy(dtype=float)
|
|
|
|
|
if not np.isfinite(x_values).all():
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非有限數值")
|
|
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
# 路徑 1: 理想浮點數路徑
|
|
|
|
|
y_float = signal.lfilter(b_vals, a_vals, x_values)
|
|
|
|
|
|
|
|
|
|
# 路徑 2: 整數模擬路徑
|
|
|
|
|
if b_int is not None and a_int is not None:
|
|
|
|
|
y_fixed = integer_lfilter(b_int, a_int, x_values, shift_in, shift_out, shift_b, shift_a, use_round)
|
|
|
|
|
else:
|
|
|
|
|
y_fixed = y_float
|
2026-05-14 09:41:00 +08:00
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
index, original, filtered_float, filtered_fixed, step = downsample_for_plot(df.index.to_numpy(), x_signal, y_float, y_fixed)
|
2026-05-14 09:41:00 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"index": index.tolist(),
|
|
|
|
|
"original": original.tolist(),
|
2026-05-17 00:58:19 +08:00
|
|
|
"filtered": filtered_float.tolist(),
|
|
|
|
|
"filtered_fixed": filtered_fixed.tolist(),
|
2026-05-14 09:41:00 +08:00
|
|
|
"col_name": col_to_filter,
|
|
|
|
|
"total_points": int(len(df.index)),
|
|
|
|
|
"plot_points": int(len(index)),
|
|
|
|
|
"downsample_step": int(step),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-05-17 00:58:19 +08:00
|
|
|
def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False):
|
|
|
|
|
path = get_cached_file_path(file_id)
|
|
|
|
|
# 匯出時需要原始所有欄位,但仍受限於 MAX_ROWS
|
|
|
|
|
df = pd.read_csv(path, nrows=MAX_ROWS)
|
|
|
|
|
|
|
|
|
|
if col_idx < 0 or col_idx >= len(df.columns):
|
|
|
|
|
raise HTTPException(status_code=400, detail="欄位索引超出範圍")
|
|
|
|
|
col_to_filter = df.columns[col_idx]
|
|
|
|
|
x_signal = pd.to_numeric(df[col_to_filter], errors="coerce")
|
|
|
|
|
x_values = x_signal.to_numpy(dtype=float)
|
|
|
|
|
|
|
|
|
|
y_float = signal.lfilter(b_vals, a_vals, x_values)
|
|
|
|
|
if b_int is not None and a_int is not None:
|
|
|
|
|
y_fixed = integer_lfilter(b_int, a_int, x_values, shift_in, shift_out, shift_b, shift_a, use_round)
|
|
|
|
|
else:
|
|
|
|
|
y_fixed = y_float
|
|
|
|
|
|
|
|
|
|
df[f"{col_to_filter}_filtered_ideal"] = y_float
|
|
|
|
|
df[f"{col_to_filter}_filtered_fixed"] = y_fixed
|
2026-05-14 09:41:00 +08:00
|
|
|
csv_buffer = io.StringIO()
|
|
|
|
|
df.to_csv(csv_buffer, index=False)
|
|
|
|
|
return csv_buffer.getvalue()
|
|
|
|
|
|