import numpy as np from fastapi import HTTPException from scipy import signal from .config import BODE_MAX_MULTIPLIER, BODE_POINTS from .schemas import BodeCascadeParams, BodeCompareParams, BodeParams from .validation import require_finite, validate_coefficients def _bode_series(b_values, a_values, f_eval, fs_val): b_vals = validate_coefficients(b_values, "b") a_vals = validate_coefficients(a_values, "a") _, h = signal.freqz(b_vals, a_vals, worN=f_eval, fs=fs_val) return { "mag": (20 * np.log10(np.abs(h) + 1e-12)).tolist(), "phase": np.angle(h, deg=True).tolist(), } def _frequency_axis(fs): fs_val = require_finite(fs, "fs") f_min = fs_val / 50000.0 f_max = fs_val * BODE_MAX_MULTIPLIER return fs_val, np.logspace(np.log10(f_min), np.log10(f_max), BODE_POINTS) def calculate_bode_response(params: BodeParams): fs_val, f_eval = _frequency_axis(params.fs) series = _bode_series(params.b, params.a, f_eval, fs_val) return { "freq": f_eval.tolist(), "mag": series["mag"], "phase": series["phase"], } def calculate_bode_compare_response(params: BodeCompareParams): fs_val, f_eval = _frequency_axis(params.ideal.fs) fixed_fs_val = require_finite(params.fixed.fs, "fixed.fs") if not np.isclose(fixed_fs_val, fs_val): raise HTTPException(status_code=400, detail="ideal 與 fixed 的 fs 必須相同") ideal = _bode_series(params.ideal.b, params.ideal.a, f_eval, fs_val) fixed = _bode_series(params.fixed.b, params.fixed.a, f_eval, fs_val) return { "freq": f_eval.tolist(), "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(), }, }