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= مأخوذة من خارج الحلقة -- هو القالب الصحيح لمحلل طيفي بثّي يعمل على ميكروفون عالي الدقة.