6.15. الترشيح والأطياف الطيفية

الترشيح والتنعيم وأطياف المقدار هي المهام المجاورة لتحويلة الـ FFT الخام: تنعيم تدفق من العينات أو ترشيحه بمرشح تمرير نطاقي، وحساب أطياف المقدار في حلقة بثّية دون تخصيص، وإعادة تفسير المخازن المؤقتة الخام للطرفيات بوصفها مصفوفات عشرية. والأدوات المتاحة:

  • sosfilt() -- مرشح رقمي يُطبَّق عبر مقاطع متتالية من الرتبة الثانية.

  • spectrogram() -- المقدار abs(fft(...)) دون تخصيصات وسيطة.

  • from_int16_buffer() وسائر الدوال المساعدة from_*_buffer في ulab.utils -- تستخلص مصفوفة عشرية من مخزن مؤقت لا تغطي الدالة المدمجة frombuffer() نوعَ بياناته.

6.15.1. الترشيح باستخدام sosfilt

تُطبّق sosfilt() مرشحًا رقميًا لاستجابة نبضية لانهائية (IIR) على هيئة سلسلة من مقاطع الرتبة الثانية (SOS) -- وهي صيغة متينة عدديًا. والـ sos هو متتالية من المقاطع ذات الطول 6؛ والـ x هو الدخل أحادي البُعد:

from ulab import numpy as np
from ulab import scipy as sp

x   = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
sos = [[1, 2, 3, 1, 5, 6],
       [1, 2, 3, 1, 5, 6]]
y   = sp.signal.sosfilt(sos, x)

يحمل كل صف من sos ستة معاملات [b0, b1, b2, a0, a1, a2] لمقطع biquad واحد. وعادةً ما تُحسب مصفوفة sos مسبقًا على حاسوب شخصي باستخدام scipy.signal.iirfilter(..., output='sos') ثم تُنسخ إلى برنامج الكاميرا النصي على هيئة قيمة Python حرفية.

تحمل الكلمة المفتاحية الاختيارية zi= حالةَ المرشح عبر المخازن المؤقتة. مرِّر حالةً ابتدائيةً بالشكل (n_sections, 2) فتُعيد الدالة (y, zf) -- الخرج المُرشَّح والحالة النهائية -- بحيث تُغذّي الحالةُ النهائيةُ لمخزن مؤقت الحالةَ الابتدائيةَ للمخزن التالي:

y0, zf0 = sp.signal.sosfilt(sos, buffer0, zi=zi)
y1, zf1 = sp.signal.sosfilt(sos, buffer1, zi=zf0)
# ...

هذا هو النمط القياسي لمرشح بثّي يعمل على بيانات مُخزّنة مؤقتًا -- دخل ميكروفون يُقرأ بمعدل 1024 عينة في المرة، وعينات ADC مُجمّعة في قطع مدفوعة بالـ DMA، وقراءات IMU مجموعة عبر نافذة.

6.15.2. الأطياف الطيفية

تحسب spectrogram() مقدار تحويلة فورييه. وهي مكافئة مفاهيميًا لـ np.sqrt(real * real + imag * imag) بعد استدعاء fft()، لكنها تطوي العمل في استدعاء واحد -- دون الاحتفاظ بـ real * real الوسيط، أو imag * imag، أو المجموع، أو مصفوفة المقدار الصريحة في الـ RAM في أي لحظة. وهذا يجعلها الأداة الصحيحة في أي حلقة تُحسب فيها الأطياف بشكل متكرر:

from ulab import numpy as np
from ulab import utils

x        = np.linspace(0, 10, num=1024)
spectrum = utils.spectrogram(x)

تعكس صيغة الوسيط صيغة fft(): مصفوفة حقيقية واحدة، أو زوج (real, imag) عندما يكون للدخل جزء تخيلي.

تساعد ثلاث كلمات مفتاحية في التخصيص:

  • scratchpad=None -- مصفوفة عشرية كثيفة أحادية البُعد بطول 2 * len(signal) تستخدمها spectrogram() كمساحة عمل.

  • out=None -- مصفوفة عشرية أحادية البُعد تُكتب النتيجة فيها.

  • log=False -- عند True، تأخذ log() للمقدار قبل الإعادة، مطويةً في الاستدعاء نفسه.

النمط البثّي هو تخصيص كل شيء مرةً واحدةً وعدم التخصيص مجددًا أبدًا:

from ulab import numpy as np
from ulab import utils

N = 1024
scratch = np.zeros(2 * N)
out     = np.zeros(N)

while True:
    signal = read_samples(N)
    utils.spectrogram(signal, out=out, scratchpad=scratch,
                      log=True)
    # out now holds the log-magnitude spectrum for this window ...

قارن بالنسخة البديهية لكن المُسرِفة:

while True:
    signal   = read_samples(N)
    spectrum = np.log(utils.spectrogram(signal))   # two allocations

تنتج النسختان الأرقامَ نفسها، لكن النسخة الأولى لا تخصص أي شيء داخل الحلقة -- إذ تُبقي الكاميرا الذاكرةَ نفسَها قيد الاستخدام في كل تكرار، وتعمل الحلقة بسرعة أكبر.

6.15.3. المخازن المؤقتة للطرفيات الأعرض من 16 بت

لا تتعامل frombuffer() إلا مع أنواع البيانات التي تعرّفها numpy نفسها (uint8 / int8، و uint16 / int16، و float). وعندما تنتج طرفيةٌ عيناتٍ صحيحةً بطول 32 بت -- مثل ADC بطول 24 أو 32 بت، أو ميكروفون عالي الدقة -- تكشف ulab.utils دوالًا مساعدةً صريحةً للتحويل:

تأخذ كلٌّ منها مخزنًا مؤقتًا شبيهًا بالبايتات وتُعيد ndarray عشرية:

from ulab import utils

buf = bytearray([1, 1, 0, 0, 0, 0, 0, 255])
utils.from_uint32_buffer(buf)
# array([257.0, 4278190080.0])

تقبل الدوال مقابض توفير التخصيص نفسها التي تقبلها spectrogram():

  • count= و offset= لتخطّي ترويسة أو لتحديد القراءة.

  • out= للكتابة في مصفوفة عشرية مُخصّصة مسبقًا.

  • byteswap=True عندما تختلف الطرفية عن الـ MCU في ترتيب البايتات.

النمط المُدمج -- استدعاء from_int32_buffer() واحد يصبّ مباشرةً في استدعاء spectrogram() واحد، وكلاهما بمخازن out= مأخوذة من خارج الحلقة -- هو القالب الصحيح لمحلل طيفي بثّي يعمل على ميكروفون عالي الدقة.