feat: integrate cascade filter workflow
This commit is contained in:
+37
-1
@@ -3,7 +3,7 @@ from fastapi import HTTPException
|
||||
from scipy import signal
|
||||
|
||||
from .config import BODE_MAX_MULTIPLIER, BODE_POINTS
|
||||
from .schemas import BodeCompareParams, BodeParams
|
||||
from .schemas import BodeCascadeParams, BodeCompareParams, BodeParams
|
||||
from .validation import require_finite, validate_coefficients
|
||||
|
||||
|
||||
@@ -47,3 +47,39 @@ def calculate_bode_compare_response(params: BodeCompareParams):
|
||||
"ideal": ideal,
|
||||
"fixed": fixed,
|
||||
}
|
||||
|
||||
|
||||
def calculate_bode_cascade_response(params: BodeCascadeParams):
|
||||
fs_val, f_eval = _frequency_axis(params.fs)
|
||||
h_total_ideal = np.ones(len(f_eval), dtype=complex)
|
||||
h_total_fixed = np.ones(len(f_eval), dtype=complex)
|
||||
any_active = False
|
||||
|
||||
for i, stage in enumerate(params.stages):
|
||||
if not stage.isActive:
|
||||
continue
|
||||
any_active = True
|
||||
b_vals = validate_coefficients(stage.b, f"stages[{i}].b")
|
||||
a_vals = validate_coefficients(stage.a, f"stages[{i}].a")
|
||||
b_fixed_vals = validate_coefficients(stage.b_fixed, f"stages[{i}].b_fixed")
|
||||
a_fixed_vals = validate_coefficients(stage.a_fixed, f"stages[{i}].a_fixed")
|
||||
_, h_ideal = signal.freqz(b_vals, a_vals, worN=f_eval, fs=fs_val)
|
||||
_, h_fixed = signal.freqz(b_fixed_vals, a_fixed_vals, worN=f_eval, fs=fs_val)
|
||||
h_total_ideal *= h_ideal
|
||||
h_total_fixed *= h_fixed
|
||||
|
||||
if not any_active:
|
||||
h_total_ideal = np.ones(len(f_eval), dtype=complex)
|
||||
h_total_fixed = np.ones(len(f_eval), dtype=complex)
|
||||
|
||||
return {
|
||||
"freq": f_eval.tolist(),
|
||||
"ideal": {
|
||||
"mag": (20 * np.log10(np.abs(h_total_ideal) + 1e-12)).tolist(),
|
||||
"phase": np.angle(h_total_ideal, deg=True).tolist(),
|
||||
},
|
||||
"fixed": {
|
||||
"mag": (20 * np.log10(np.abs(h_total_fixed) + 1e-12)).tolist(),
|
||||
"phase": np.angle(h_total_fixed, deg=True).tolist(),
|
||||
},
|
||||
}
|
||||
|
||||
+75
-19
@@ -138,7 +138,43 @@ def integer_lfilter(b_int, a_int, x_float, shift_in, shift_out, shift_b, shift_a
|
||||
return y_out.astype(float) / (2**shift_out)
|
||||
|
||||
|
||||
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):
|
||||
def _run_filter_paths(x_values, b_vals, a_vals, b_int, a_int, shift_in, shift_out, shift_b, shift_a, use_round, stages=None):
|
||||
if stages is None:
|
||||
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
|
||||
return y_float, y_fixed
|
||||
|
||||
y_float = x_values
|
||||
y_fixed = x_values
|
||||
any_active = False
|
||||
for stage in stages:
|
||||
if not stage.get("isActive", True):
|
||||
continue
|
||||
any_active = True
|
||||
y_float = signal.lfilter(stage["b"], stage["a"], y_float)
|
||||
if stage.get("b_int") is not None and stage.get("a_int") is not None:
|
||||
y_fixed = integer_lfilter(
|
||||
stage["b_int"],
|
||||
stage["a_int"],
|
||||
y_fixed,
|
||||
stage["shift_in"],
|
||||
stage["shift_out"],
|
||||
stage["shift_b"],
|
||||
stage["shift_a"],
|
||||
stage["use_round"],
|
||||
)
|
||||
else:
|
||||
y_fixed = y_float
|
||||
|
||||
if not any_active:
|
||||
return x_values, x_values
|
||||
return y_float, y_fixed
|
||||
|
||||
|
||||
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, stages=None, start_idx=None, end_idx=None):
|
||||
path = get_cached_file_path(file_id)
|
||||
|
||||
# 預先讀取欄位名稱,避免用 usecols 讀取後找不到原始索引
|
||||
@@ -157,16 +193,27 @@ def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=
|
||||
if not np.isfinite(x_values).all():
|
||||
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非有限數值")
|
||||
|
||||
# 路徑 1: 理想浮點數路徑
|
||||
y_float = signal.lfilter(b_vals, a_vals, x_values)
|
||||
y_float, y_fixed = _run_filter_paths(
|
||||
x_values, b_vals, a_vals, b_int, a_int,
|
||||
shift_in, shift_out, shift_b, shift_a, use_round, stages=stages,
|
||||
)
|
||||
|
||||
# 路徑 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
|
||||
total_points = len(df.index)
|
||||
start_idx_clean = start_idx if isinstance(start_idx, (int, float, str)) else None
|
||||
end_idx_clean = end_idx if isinstance(end_idx, (int, float, str)) else None
|
||||
s_idx = max(0, int(start_idx_clean)) if start_idx_clean is not None else 0
|
||||
e_idx = min(total_points, int(end_idx_clean)) if end_idx_clean is not None else total_points
|
||||
if s_idx >= e_idx:
|
||||
s_idx = 0
|
||||
e_idx = total_points
|
||||
|
||||
index, original, filtered_float, filtered_fixed, step = downsample_for_plot(df.index.to_numpy(), x_signal, y_float, y_fixed)
|
||||
full_index = df.index.to_numpy()
|
||||
index, original, filtered_float, filtered_fixed, step = downsample_for_plot(
|
||||
full_index[s_idx:e_idx],
|
||||
x_values[s_idx:e_idx],
|
||||
y_float[s_idx:e_idx],
|
||||
y_fixed[s_idx:e_idx],
|
||||
)
|
||||
|
||||
return {
|
||||
"index": index.tolist(),
|
||||
@@ -174,13 +221,13 @@ def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=
|
||||
"filtered": filtered_float.tolist(),
|
||||
"filtered_fixed": filtered_fixed.tolist(),
|
||||
"col_name": col_to_filter,
|
||||
"total_points": int(len(df.index)),
|
||||
"total_points": int(total_points),
|
||||
"plot_points": int(len(index)),
|
||||
"downsample_step": int(step),
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
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, stages=None, fs=100000.0, compact=False):
|
||||
path = get_cached_file_path(file_id)
|
||||
# 匯出時需要原始所有欄位,但仍受限於 MAX_ROWS
|
||||
df = pd.read_csv(path, nrows=MAX_ROWS)
|
||||
@@ -191,14 +238,23 @@ def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None,
|
||||
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
|
||||
y_float, y_fixed = _run_filter_paths(
|
||||
x_values, b_vals, a_vals, b_int, a_int,
|
||||
shift_in, shift_out, shift_b, shift_a, use_round, stages=stages,
|
||||
)
|
||||
|
||||
if compact:
|
||||
export_df = pd.DataFrame({
|
||||
"Time (s)": df.index.to_numpy(dtype=float) / fs,
|
||||
col_to_filter: x_values,
|
||||
f"{col_to_filter}_filtered_ideal": y_float,
|
||||
f"{col_to_filter}_filtered_fixed": y_fixed,
|
||||
})
|
||||
else:
|
||||
export_df = df.copy()
|
||||
export_df[f"{col_to_filter}_filtered_ideal"] = y_float
|
||||
export_df[f"{col_to_filter}_filtered_fixed"] = y_fixed
|
||||
|
||||
df[f"{col_to_filter}_filtered_ideal"] = y_float
|
||||
df[f"{col_to_filter}_filtered_fixed"] = y_fixed
|
||||
csv_buffer = io.StringIO()
|
||||
df.to_csv(csv_buffer, index=False)
|
||||
export_df.to_csv(csv_buffer, index=False)
|
||||
return csv_buffer.getvalue()
|
||||
|
||||
@@ -47,3 +47,16 @@ class BodeCompareParams(BaseModel):
|
||||
class MCUWriteParams(BaseModel):
|
||||
command: str = Field(min_length=1, max_length=128)
|
||||
port: Optional[str] = Field(default=None, max_length=256)
|
||||
|
||||
|
||||
class CascadeStageParams(BaseModel):
|
||||
b: List[float] = Field(min_length=1, max_length=MAX_COEFFICIENTS)
|
||||
a: List[float] = Field(min_length=1, max_length=MAX_COEFFICIENTS)
|
||||
b_fixed: List[float] = Field(min_length=1, max_length=MAX_COEFFICIENTS)
|
||||
a_fixed: List[float] = Field(min_length=1, max_length=MAX_COEFFICIENTS)
|
||||
isActive: bool = True
|
||||
|
||||
|
||||
class BodeCascadeParams(BaseModel):
|
||||
stages: List[CascadeStageParams] = Field(min_length=1)
|
||||
fs: float = Field(gt=0)
|
||||
|
||||
Reference in New Issue
Block a user