Skip to content

Commit 08ea8ad

Browse files
committed
wip: advanced filter
1 parent fe2065b commit 08ea8ad

File tree

4 files changed

+59563
-0
lines changed

4 files changed

+59563
-0
lines changed
315 KB
Loading
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Générateur de valeurs de référence pour le filtre passe-bande BandpassFilter
4+
5+
Ce script utilise scipy.signal pour générer des valeurs de référence qui serviront
6+
à valider l'implémentation Rust du BandpassFilter pour différents ordres (2, 4, 6, 8, 10).
7+
8+
Il génère des signaux de test avec différentes fréquences et calcule la réponse
9+
du filtre scipy pour chaque configuration, puis sauvegarde les résultats en JSON
10+
pour les tests d'intégration Rust.
11+
"""
12+
13+
import numpy as np
14+
import scipy.signal as signal
15+
import json
16+
import matplotlib.pyplot as plt
17+
from pathlib import Path
18+
19+
# Configuration des tests
20+
SAMPLE_RATE = 48000
21+
CENTER_FREQ = 1000.0 # Hz
22+
BANDWIDTH = 200.0 # Hz
23+
ORDERS = [2, 4, 6, 8, 10]
24+
SIGNAL_LENGTH = 1024 # Nombre d'échantillons
25+
26+
def create_test_signals():
27+
"""Crée différents signaux de test pour valider le filtre"""
28+
t = np.arange(SIGNAL_LENGTH) / SAMPLE_RATE
29+
30+
signals = {}
31+
32+
# Signal sinusoïdal à la fréquence centrale (devrait passer)
33+
signals['center_freq'] = np.sin(2 * np.pi * CENTER_FREQ * t)
34+
35+
# Signal sinusoïdal dans la bande passante (devrait passer)
36+
signals['in_band'] = np.sin(2 * np.pi * (CENTER_FREQ + BANDWIDTH/4) * t)
37+
38+
# Signal sinusoïdal en dehors de la bande (devrait être atténué)
39+
signals['out_of_band_low'] = np.sin(2 * np.pi * (CENTER_FREQ - BANDWIDTH) * t)
40+
signals['out_of_band_high'] = np.sin(2 * np.pi * (CENTER_FREQ + BANDWIDTH) * t)
41+
42+
# Signal à impulsion (pour tester la réponse impulsionnelle)
43+
impulse = np.zeros(SIGNAL_LENGTH)
44+
impulse[0] = 1.0
45+
signals['impulse'] = impulse
46+
47+
# Signal de bruit blanc (pour tester le filtrage du bruit)
48+
np.random.seed(42) # Pour la reproductibilité
49+
signals['white_noise'] = np.random.normal(0, 0.1, SIGNAL_LENGTH)
50+
51+
# Signal multi-fréquences
52+
multi_freq = (np.sin(2 * np.pi * 500 * t) + # En dessous de la bande
53+
np.sin(2 * np.pi * CENTER_FREQ * t) + # Dans la bande
54+
np.sin(2 * np.pi * 2000 * t)) # Au-dessus de la bande
55+
signals['multi_freq'] = multi_freq
56+
57+
return signals
58+
59+
def design_scipy_bandpass_filter(order, center_freq, bandwidth, sample_rate):
60+
"""
61+
Conçoit un filtre passe-bande Butterworth avec scipy.signal
62+
63+
Args:
64+
order: Ordre du filtre (doit être pair)
65+
center_freq: Fréquence centrale en Hz
66+
bandwidth: Largeur de bande en Hz
67+
sample_rate: Fréquence d'échantillonnage en Hz
68+
69+
Returns:
70+
sos: Sections du filtre au format Second-Order Sections
71+
"""
72+
# Calcul des fréquences de coupure
73+
low_freq = center_freq - bandwidth / 2
74+
high_freq = center_freq + bandwidth / 2
75+
76+
# Normalisation par la fréquence de Nyquist
77+
nyquist = sample_rate / 2
78+
low_norm = low_freq / nyquist
79+
high_norm = high_freq / nyquist
80+
81+
# Vérification des limites
82+
if low_norm <= 0 or high_norm >= 1:
83+
raise ValueError(f"Frequencies out of range: {low_freq}-{high_freq} Hz for Fs={sample_rate} Hz")
84+
85+
# Conception du filtre passe-bande Butterworth
86+
# scipy.signal.butter produit automatiquement des sections SOS optimisées
87+
sos = signal.butter(order, [low_norm, high_norm],
88+
btype='band', analog=False, output='sos')
89+
90+
return sos
91+
92+
def compute_frequency_response(sos, sample_rate, n_points=1024):
93+
"""Calcule la réponse en fréquence du filtre"""
94+
w, h = signal.sosfreqz(sos, worN=n_points, fs=sample_rate)
95+
return w.tolist(), np.abs(h).tolist(), np.angle(h).tolist()
96+
97+
def process_signals_with_filter(signals, sos):
98+
"""Traite tous les signaux de test avec le filtre scipy"""
99+
processed = {}
100+
101+
for signal_name, signal_data in signals.items():
102+
# Application du filtre avec sosfilt (recommandé pour la stabilité numérique)
103+
filtered = signal.sosfilt(sos, signal_data)
104+
processed[signal_name] = filtered.tolist()
105+
106+
return processed
107+
108+
def generate_reference_data():
109+
"""Génère toutes les données de référence pour tous les ordres"""
110+
print("Génération des signaux de test...")
111+
test_signals = create_test_signals()
112+
113+
reference_data = {
114+
'config': {
115+
'sample_rate': SAMPLE_RATE,
116+
'center_freq': CENTER_FREQ,
117+
'bandwidth': BANDWIDTH,
118+
'signal_length': SIGNAL_LENGTH
119+
},
120+
'test_signals': {name: data.tolist() for name, data in test_signals.items()},
121+
'filters': {}
122+
}
123+
124+
for order in ORDERS:
125+
print(f"Traitement de l'ordre {order}...")
126+
127+
# Conception du filtre scipy
128+
sos = design_scipy_bandpass_filter(order, CENTER_FREQ, BANDWIDTH, SAMPLE_RATE)
129+
130+
# Affichage des coefficients pour debug
131+
print(f" Coefficients SOS pour l'ordre {order}:")
132+
for i, section in enumerate(sos):
133+
b = section[:3] # b0, b1, b2
134+
a = section[3:] # a0, a1, a2 (a0 normalisé à 1.0)
135+
print(f" Section {i}: b=[{b[0]:.6f}, {b[1]:.6f}, {b[2]:.6f}], a=[{a[0]:.6f}, {a[1]:.6f}, {a[2]:.6f}]")
136+
137+
# Calcul de la réponse en fréquence
138+
freqs, magnitude, phase = compute_frequency_response(sos, SAMPLE_RATE)
139+
140+
# Traitement des signaux de test
141+
processed_signals = process_signals_with_filter(test_signals, sos)
142+
143+
# Stockage des résultats pour cet ordre
144+
reference_data['filters'][f'order_{order}'] = {
145+
'sos_coefficients': sos.tolist(),
146+
'frequency_response': {
147+
'frequencies': freqs,
148+
'magnitude': magnitude,
149+
'phase': phase
150+
},
151+
'processed_signals': processed_signals
152+
}
153+
154+
return reference_data
155+
156+
def save_reference_data(data, output_file):
157+
"""Sauvegarde les données de référence en JSON"""
158+
output_path = Path(output_file)
159+
output_path.parent.mkdir(parents=True, exist_ok=True)
160+
161+
with open(output_path, 'w') as f:
162+
json.dump(data, f, indent=2)
163+
164+
print(f"Données de référence sauvegardées dans: {output_path}")
165+
166+
def plot_frequency_responses(reference_data):
167+
"""Génère des graphiques de comparaison des réponses en fréquence"""
168+
plt.figure(figsize=(12, 8))
169+
170+
for order in ORDERS:
171+
filter_data = reference_data['filters'][f'order_{order}']
172+
freqs = np.array(filter_data['frequency_response']['frequencies'])
173+
magnitude = np.array(filter_data['frequency_response']['magnitude'])
174+
175+
# Conversion en dB
176+
magnitude_db = 20 * np.log10(np.maximum(magnitude, 1e-10))
177+
178+
plt.semilogx(freqs, magnitude_db, label=f'Ordre {order}')
179+
180+
plt.axvline(CENTER_FREQ - BANDWIDTH/2, color='red', linestyle='--', alpha=0.7, label='Fc - BW/2')
181+
plt.axvline(CENTER_FREQ, color='black', linestyle='-', alpha=0.7, label='Fc')
182+
plt.axvline(CENTER_FREQ + BANDWIDTH/2, color='red', linestyle='--', alpha=0.7, label='Fc + BW/2')
183+
184+
plt.xlabel('Fréquence (Hz)')
185+
plt.ylabel('Magnitude (dB)')
186+
plt.title(f'Réponse en fréquence - Filtre passe-bande Butterworth\nFc={CENTER_FREQ}Hz, BW={BANDWIDTH}Hz')
187+
plt.grid(True, alpha=0.3)
188+
plt.legend()
189+
plt.ylim(-80, 5)
190+
191+
# Sauvegarder le graphique
192+
plot_path = Path(__file__).parent / 'bandpass_frequency_response.png'
193+
plt.savefig(plot_path, dpi=300, bbox_inches='tight')
194+
print(f"Graphique sauvegardé dans: {plot_path}")
195+
196+
plt.show()
197+
198+
def validate_filter_performance(reference_data):
199+
"""Valide que les filtres ont les caractéristiques attendues"""
200+
print("\nValidation des performances des filtres:")
201+
print("="*50)
202+
203+
for order in ORDERS:
204+
filter_data = reference_data['filters'][f'order_{order}']
205+
freqs = np.array(filter_data['frequency_response']['frequencies'])
206+
magnitude = np.array(filter_data['frequency_response']['magnitude'])
207+
208+
# Trouver l'indice de la fréquence centrale
209+
center_idx = np.argmin(np.abs(freqs - CENTER_FREQ))
210+
center_magnitude = magnitude[center_idx]
211+
212+
# Trouver les indices des fréquences de coupure
213+
low_cutoff_idx = np.argmin(np.abs(freqs - (CENTER_FREQ - BANDWIDTH/2)))
214+
high_cutoff_idx = np.argmin(np.abs(freqs - (CENTER_FREQ + BANDWIDTH/2)))
215+
216+
low_cutoff_magnitude = magnitude[low_cutoff_idx]
217+
high_cutoff_magnitude = magnitude[high_cutoff_idx]
218+
219+
# Calculer l'atténuation aux fréquences de coupure (devrait être ~-3dB)
220+
low_cutoff_db = 20 * np.log10(low_cutoff_magnitude / center_magnitude)
221+
high_cutoff_db = 20 * np.log10(high_cutoff_magnitude / center_magnitude)
222+
223+
print(f"Ordre {order:2d}:")
224+
print(f" Magnitude à Fc: {center_magnitude:.4f}")
225+
print(f" Atténuation à Fc-BW/2: {low_cutoff_db:.2f} dB")
226+
print(f" Atténuation à Fc+BW/2: {high_cutoff_db:.2f} dB")
227+
228+
# Calculer la pente de coupure (approximation)
229+
# Chercher la fréquence où l'atténuation est de -20dB
230+
target_magnitude = center_magnitude * 10**(-20/20)
231+
232+
# Côté bas
233+
low_side_idx = np.where((freqs < CENTER_FREQ) & (magnitude < target_magnitude))
234+
if len(low_side_idx[0]) > 0:
235+
low_20db_freq = freqs[low_side_idx[0][-1]]
236+
low_slope_freq_range = (CENTER_FREQ - BANDWIDTH/2) - low_20db_freq
237+
if low_slope_freq_range > 0:
238+
# Approximation de la pente: (20-3)/log10(freq_ratio)
239+
freq_ratio = (CENTER_FREQ - BANDWIDTH/2) / low_20db_freq
240+
low_slope = 17 / np.log10(freq_ratio) # dB/decade
241+
print(f" Pente côté bas: ~{low_slope:.1f} dB/decade")
242+
243+
print()
244+
245+
if __name__ == "__main__":
246+
print("Générateur de valeurs de référence pour BandpassFilter")
247+
print("="*60)
248+
249+
# Génération des données de référence
250+
reference_data = generate_reference_data()
251+
252+
# Validation des performances
253+
validate_filter_performance(reference_data)
254+
255+
# Sauvegarde des données
256+
output_file = Path(__file__).parent / '../tests/data/bandpass_reference_values.json'
257+
save_reference_data(reference_data, output_file)
258+
259+
# Génération des graphiques
260+
plot_frequency_responses(reference_data)
261+
262+
print("\nGénération terminée avec succès!")
263+
print("Les données de référence sont prêtes pour les tests Rust.")

0 commit comments

Comments
 (0)