Files
tcad-bodeplot/dea/bode.py
T

106 lines
3.4 KiB
Python
Executable File

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
# Ideal path
b_vals = validate_coefficients(stage.b, f"stages[{i}].b")
a_vals = validate_coefficients(stage.a, f"stages[{i}].a")
_, h_ideal = signal.freqz(b_vals, a_vals, worN=f_eval, fs=fs_val)
h_total_ideal *= h_ideal
# Fixed path
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_fixed = signal.freqz(b_fixed_vals, a_fixed_vals, worN=f_eval, fs=fs_val)
h_total_fixed *= h_fixed
if not any_active:
return {
"freq": f_eval.tolist(),
"ideal": {
"mag": np.zeros(len(f_eval)).tolist(),
"phase": np.zeros(len(f_eval)).tolist(),
},
"fixed": {
"mag": np.zeros(len(f_eval)).tolist(),
"phase": np.zeros(len(f_eval)).tolist(),
}
}
mag_ideal = (20 * np.log10(np.abs(h_total_ideal) + 1e-12)).tolist()
phase_ideal = np.angle(h_total_ideal, deg=True).tolist()
mag_fixed = (20 * np.log10(np.abs(h_total_fixed) + 1e-12)).tolist()
phase_fixed = np.angle(h_total_fixed, deg=True).tolist()
return {
"freq": f_eval.tolist(),
"ideal": {
"mag": mag_ideal,
"phase": phase_ideal,
},
"fixed": {
"mag": mag_fixed,
"phase": phase_fixed,
}
}