پس از تغییر کد فیلتر، برابر بودن طول خروجی با ورودی برای تشخیص سالم‌ماندن رفتار کافی نیست. ممکن است ترتیب ضرایب وارونه شده باشد، حافظه در آغاز هر بسته صفر شود یا انتهای پاسخ بی‌دلیل حذف و اضافه شود. در این درس، یک FIR علّی کوچک را با نمونه‌های ولتاژ ساختگی می‌سازیم و برای نگهداری آزمون‌هایش جواب دستی و مرجع مستقل داریم. طراحی پاسخ فرکانسی، حذف نویز واقعی یا کارایی دستگاه موضوع این آزمایش نیست.

۱. ضرایب از نمونهٔ جاری به گذشته مرتب‌اند

نمونهٔ جاری و گذشته در جمع وزن‌دار
تصویر مفهومی: نمونهٔ جاری و گذشته در جمع وزن‌دار؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای M ضریب، خروجی نمونهٔ n برابر جمع h[k]×x[n−k] از k صفر تا M−1 است. پس h[0] در نمونهٔ جاری ضرب می‌شود، h[1] در یک نمونهٔ قبل و به همین ترتیب. نمونهٔ آینده وارد جمع نمی‌شود. در مثال، ضرایب ۰٫۵، ۰٫۲۵ و منفی ۰٫۱۲۵ هستند؛ عمداً متقارن انتخاب نشده‌اند تا وارونگی ترتیب در آزمون پنهان نماند.

معادلهٔ تفاضلی lfilter در SciPy سهم نمونهٔ جاری و گذشته را نشان می‌دهد. در FIR این تمرین، بخش بازخورد خروجی نداریم. کد خودمان را اجرا می‌کنیم و SciPy اجرا نشده است. حافظهٔ ما نمونه‌های ورودیِ گذشته را نگه می‌دارد؛ آن را با متغیرهای داخلی ساختار direct-II و zi یا zf یکسان معرفی نمی‌کنیم.

نمونه‌ها و تاریخچه واحد ولت دارند، ضرایب بی‌بعدند و خروجی هم ولت است. جمع گسسته را در فاصلهٔ زمانی ضرب نمی‌کنیم؛ اینجا انتگرال هم‌نهشتی پیوسته نیست. برنامه روی اندیس نمونه عمل می‌کند، زمان یا نرخ نمونه‌برداری نمی‌گیرد و تغییر نرخ هم انجام نمی‌دهد. از انتخاب این ضرایب، گذرپایین بودن یا تأخیر زمانیِ تأییدشده نتیجه نمی‌گیریم؛ هدف، آزمودن قرارداد محاسبه است.

۲. آغاز صفر، حافظهٔ ورودی و طول خروجی

حالت آغاز صفر و خروجی هم‌طول ورودی
تصویر مفهومی: حالت آغاز صفر و خروجی هم‌طول ورودی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

وقتی history داده نشده باشد، M−1 نمونهٔ پیش از شروع را صفر فرض می‌کنیم. تاریخچهٔ صریح از قدیمی‌ترین به تازه‌ترین مرتب است؛ برای سه ضریب دو مقدار لازم داریم. در محاسبهٔ هر خروجی، نمونهٔ جاری جلوی ترتیبِ معکوس این تاریخچه قرار می‌گیرد. بعد، قدیمی‌ترین نمونه کنار می‌رود و مقدار تازه وارد حافظه می‌شود. این قرارداد ترتیب باید در مستندات و آزمون باقی بماند.

هر فراخوانی با N ورودی دقیقاً N خروجی دارد و انتهای پاسخ خودکار تخلیه نمی‌شود. راهنمای convolve در NumPy برای full دو دنبالهٔ غیرخالی، طول N+M−1 را توضیح می‌دهد. خروجی ما قسمت علّیِ نخستِ آن در حالت آغاز صفر است؛ آن را حالت same کتابخانه نمی‌نامیم. برابر بودن طول، هم‌ترازی یکسان را ثابت نمی‌کند.

برای دیدن دنبالهٔ باقی‌مانده، باید صریحاً M−1 صفرِ تازه و همان تاریخچه را به تابع بدهیم. این کار فرض می‌کند پس از دادهٔ محدود، ورودی صفر است؛ نبود داده را اندازه‌گیری صفر اعلام نمی‌کند. برای یک ضریب، تاریخچه طول صفر دارد و چیزی برای تخلیه نیست. فهرست ضرایب خالی رد می‌شود، ولی فهرست غیرخالی با همهٔ ضرایب صفر، قرارداد معتبر دیگری است.

۳. کد کامل با ورودی محدود و خطای عددی آشکار

کد علّی با ضرایب ثابت و واحد ولت
تصویر مفهومی: کد علّی با ضرایب ثابت و واحد ولت؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. نمونه، ضریب و تاریخچه باید list یا tuple محدود از int یا float دقیق و متناهی باشند؛ bool، NaN، بی‌نهایت، رشته و عددِ غیرقابل‌نمایش رد می‌شوند. واحد داده و تاریخچه دقیقاً V است. کل بسته و طول حافظه پیش از محاسبه اعتبارسنجی می‌شوند؛ برنامه ورودی نامعتبر را به صفر تبدیل نمی‌کند و نمونهٔ خراب را بی‌خبر حذف نمی‌کند.

"""Original causal FIR stream example; pure batches and explicit input history."""
from fractions import Fraction
import itertools
import json
import math
import platform


def _finite_sequence(values, field):
    if type(values) not in (list, tuple):
        raise TypeError(field + ' must be finite list or tuple')
    result = []
    for value in values:
        if type(value) not in (int, float):
            raise TypeError(field + ' must contain exact int or float')
        try:
            value = float(value)
        except OverflowError as error:
            raise ValueError(field + ' cannot be represented as finite float') from error
        if not math.isfinite(value):
            raise ValueError(field + ' must be finite')
        result.append(value)
    return result


def fir_batch(samples, coefficients, history=None, unit='V'):
    """h[0]*x[n]; history is M-1 preceding inputs in oldest-to-newest order."""
    if type(unit) is not str or unit != 'V':
        raise ValueError('sample/history unit must be V')
    taps = _finite_sequence(coefficients, 'coefficients')
    if not taps:
        raise ValueError('at least one dimensionless coefficient is required')
    values = _finite_sequence(samples, 'samples')
    needed = len(taps) - 1
    memory = [0.0] * needed if history is None else _finite_sequence(history, 'history')
    if len(memory) != needed:
        raise ValueError('history length must be M-1')
    outputs = []
    # Work on private copies only; reject invalid outputs before returning any batch.
    for value in values:
        delays = [value] + list(reversed(memory))
        products = [coefficient * sample for coefficient, sample in zip(taps, delays)]
        if not all(math.isfinite(product) for product in products):
            raise ArithmeticError('nonfinite FIR product')
        try:
            output = math.fsum(products)
        except OverflowError as error:
            raise ArithmeticError('FIR summation overflow') from error
        if not math.isfinite(output):
            raise ArithmeticError('nonfinite FIR output')
        outputs.append(output)
        memory = (memory + [value])[-needed:] if needed else []
    return {'output_V': outputs, 'history_V': memory}


def reference_full(samples, coefficients):
    """Independent exact rational scatter-add full convolution, initial rest."""
    if not samples:
        return []
    result = [Fraction(0) for _ in range(len(samples) + len(coefficients) - 1)]
    for sample_index, sample in enumerate(samples):
        for coefficient_index, coefficient in enumerate(coefficients):
            result[sample_index + coefficient_index] += Fraction(sample) * Fraction(coefficient)
    return [float(value) for value in result]


def _close_sequence(left, right):
    return len(left) == len(right) and all(math.isclose(a, b, rel_tol=1e-12, abs_tol=1e-12)
                                           for a, b in zip(left, right))


def _raises(expected, function, *args, **kwargs):
    try:
        function(*args, **kwargs)
    except expected:
        return True
    return False


def run_example():
    samples = [2.0, 0.0, -2.0, 4.0, 1.0]
    coefficients = [0.5, 0.25, -0.125]
    golden = [1.0, 0.5, -1.25, 1.5, 1.75]
    main = fir_batch(samples, coefficients)
    tail = fir_batch([0.0, 0.0], coefficients, main['history_V'])
    full_golden = golden + [-0.25, -0.125]
    comparisons = []
    for count in range(1, 5):
        for program in itertools.product((-1.0, 0.0, 2.0), repeat=count):
            for taps in (coefficients, [1.0, -1.0], [0.25, 0.5, 0.25]):
                comparisons.append(_close_sequence(fir_batch(program, taps)['output_V'],
                                                    reference_full(program, taps)[:count]))
    split_checks = []
    for split in range(len(samples) + 1):
        first = fir_batch(samples[:split], coefficients)
        last = fir_batch(samples[split:], coefficients, first['history_V'])
        split_checks.append(first['output_V'] + last['output_V'] == main['output_V']
                            and last['history_V'] == main['history_V'])
    first_reset = fir_batch(samples[:2], coefficients)
    last_reset = fir_batch(samples[2:], coefficients)
    reset_output = first_reset['output_V'] + last_reset['output_V']
    other = [1.0, -1.0, 0.0, 2.0, 1.0]
    combined = [2.0 * a - b for a, b in zip(samples, other)]
    linear_expected = [2.0 * a - b for a, b in zip(main['output_V'], fir_batch(other, coefficients)['output_V'])]
    bad_numbers = (True, '1', None, 1j, float('nan'), float('inf'), -float('inf'), 2**10000)
    history = [0.0, 0.0]
    saved_samples, saved_coefficients, saved_history = samples.copy(), coefficients.copy(), history.copy()
    overflow_rejected = (_raises(ArithmeticError, fir_batch, [0.0, 1e308], [2.0], [])
                         and _raises(ArithmeticError, fir_batch, [1e308], [1.0, 1.0], [1e308]))
    late_invalid = _raises(ValueError, fir_batch, [1.0, float('nan')], coefficients, history)
    fir_batch(samples, coefficients, history)
    checks = {
        'hand_asymmetric_golden_output': main['output_V'] == golden,
        'hand_final_oldest_first_history': main['history_V'] == [4.0, 1.0],
        'unit_impulse_preserves_tap_order': fir_batch([1, 0, 0, 0], coefficients)['output_V'] == [0.5, 0.25, -0.125, 0.0],
        'explicit_zero_flush_matches_full_convolution': main['output_V'] + tail['output_V'] == full_golden
            and reference_full(samples, coefficients) == full_golden and tail['history_V'] == [0.0, 0.0],
        'independent_rational_scatter_reference': len(comparisons) == 360 and all(comparisons),
        'all_batch_splits_carry_input_history': all(split_checks),
        'reset_at_split_exposes_regression': reset_output != golden and reset_output[2] == -1.0 and golden[2] == -1.25,
        'hand_nonzero_initial_history': fir_batch([1, 0], coefficients, [-4, 2]) ==
            {'output_V': [1.5, 0.0], 'history_V': [1.0, 0.0]},
        'empty_batch_preserves_history': fir_batch([], coefficients, [-4, 2]) ==
            {'output_V': [], 'history_V': [-4.0, 2.0]} and fir_batch([], [1]) == {'output_V': [], 'history_V': []},
        'one_tap_has_no_history': fir_batch(samples, [-2]) ==
            {'output_V': [-4.0, 0.0, 4.0, -8.0, -2.0], 'history_V': []},
        'zero_taps_are_allowed_nonempty': fir_batch(samples, [0, 0]) ==
            {'output_V': [0.0] * len(samples), 'history_V': [1.0]},
        'constant_input_settles_to_tap_sum': fir_batch([4, 4, 4, 4], coefficients)['output_V'] == [2.0, 3.0, 2.5, 2.5],
        'finite_fixture_linearity': _close_sequence(fir_batch(combined, coefficients)['output_V'], linear_expected),
        'zero_prefix_delays_output': fir_batch([0.0] + samples, coefficients)['output_V'] == [0.0] + golden,
        'invalid_coefficients_rejected': all(_raises((TypeError, ValueError), fir_batch, [], c)
            for c in ([], '123', iter([1]), [True], [float('nan')], [float('inf')], [2**10000])),
        'invalid_sample_history_and_unit_contract': all(
            _raises((TypeError, ValueError), fir_batch, [value], coefficients) for value in bad_numbers)
            and all(_raises((TypeError, ValueError), fir_batch, [], coefficients, h)
                    for h in ([], [1], [1, 2, 3], [0, True], [0, float('nan')], iter([0, 0])))
            and _raises(TypeError, fir_batch, iter([1]), coefficients)
            and all(_raises(ValueError, fir_batch, [], coefficients, unit=unit) for unit in ('mV', b'V', None)),
        'overflow_and_late_invalid_do_not_mutate_inputs': overflow_rejected and late_invalid
            and samples == saved_samples and coefficients == saved_coefficients and history == saved_history,
        'every_prefix_has_no_future_dependency': all(
            fir_batch(samples[:stop], coefficients)['output_V'] == golden[:stop]
            for stop in range(len(samples) + 1)),
    }
    assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values())
    return {'runtime': {'python': platform.python_version(), 'libraries': ['stdlib only']},
            'contract': {'sample_unit': 'V', 'coefficient_unit': 'dimensionless',
                         'tap_order': 'h[0]*x[n] + h[1]*x[n-1] + ...',
                         'initial_history': 'M-1 zero inputs when history=None',
                         'history_order': 'oldest to newest', 'outputs': 'N input samples produce exactly N outputs, no automatic tail'},
            'metrics': {'samples_V': samples, 'coefficients': coefficients, 'output_V': main['output_V'],
                        'final_history_V': main['history_V'], 'explicit_flush_V': tail['output_V'],
                        'full_convolution_V': full_golden, 'reset_split_output_V': reset_output,
                        'independent_reference_cases': len(comparisons), 'batch_split_comparisons': len(split_checks)},
            'checks': checks,
            'limitations': ['Own input-history representation is not SciPy direct-II zi/zf; no SciPy/NumPy execution.',
                            'No resampling, continuous-time integration, dt multiplier, frequency response design or noise/performance claim.',
                            'Finite input/products/fsum required; overflow rejected, not universally prevented; rounding and underflow remain possible.',
                            'Finite golden/rational fixtures are evidence, not proof for all floating-point arrays or device validation.']}


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

برای جمع حاصل‌ضرب‌ها از fsum در پایتون استفاده می‌کنیم. ابتدا متناهی بودن هر حاصل‌ضرب بررسی می‌شود؛ سرریز جمع نیز خطای ArithmeticError می‌دهد. این روش همهٔ سرریزها را پیشگیری نمی‌کند و دقت نامحدود نمی‌سازد. گردکردن حاصل‌ضرب و کم‌ریزی مقدارهای بسیار کوچک همچنان ممکن است؛ موفقیت داده‌های انتخابی را به همهٔ آرایه‌های شناور تعمیم نمی‌دهیم.

تابع روی نسخهٔ خصوصی تاریخچه کار می‌کند و نتیجه را پس از پایان موفق برمی‌گرداند. خطا در نمونهٔ آخر، تاریخچهٔ متعلق به فراخواننده را نیمه‌به‌روزشده باقی نمی‌گذارد. این ویژگی از تابع خالص و کپی ورودی ناشی می‌شود؛ تراکنش یا تضمین هم‌زمانی دریافت زنده نیست. خروجی‌ها هم محدود و در حافظه‌اند؛ فیلتر علّی بودن، به‌تنهایی پردازش بی‌درنگ یا ظرفیت دریافت مشخص را تضمین نمی‌کند.

۴. پاسخ طلایی، هم‌نهشتی مستقل و شکست مرز بسته

پاسخ ضربه و مرجع مستقل هم‌نهشتی
تصویر مفهومی: پاسخ ضربه و مرجع مستقل هم‌نهشتی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ورودی دو، صفر، منفی دو، چهار و یک ولت است. با آغاز صفر، پاسخ دستی یک، ۰٫۵، منفی ۱٫۲۵، ۱٫۵ و ۱٫۷۵ ولت می‌شود. برای خروجی سوم، منفی یک از نمونهٔ جاری و منفی ۰٫۲۵ از نمونهٔ دو گام قبل داریم. حافظهٔ نهایی چهار و یک ولت است؛ با دو صفرِ صریح، منفی ۰٫۲۵ و منفی ۰٫۱۲۵ ولتِ انتهای پاسخ به دست می‌آید.

{
  "runtime": {
    "python": "3.14.5",
    "libraries": [
      "stdlib only"
    ]
  },
  "contract": {
    "sample_unit": "V",
    "coefficient_unit": "dimensionless",
    "tap_order": "h[0]*x[n] + h[1]*x[n-1] + ...",
    "initial_history": "M-1 zero inputs when history=None",
    "history_order": "oldest to newest",
    "outputs": "N input samples produce exactly N outputs, no automatic tail"
  },
  "metrics": {
    "samples_V": [
      2.0,
      0.0,
      -2.0,
      4.0,
      1.0
    ],
    "coefficients": [
      0.5,
      0.25,
      -0.125
    ],
    "output_V": [
      1.0,
      0.5,
      -1.25,
      1.5,
      1.75
    ],
    "final_history_V": [
      4.0,
      1.0
    ],
    "explicit_flush_V": [
      -0.25,
      -0.125
    ],
    "full_convolution_V": [
      1.0,
      0.5,
      -1.25,
      1.5,
      1.75,
      -0.25,
      -0.125
    ],
    "reset_split_output_V": [
      1.0,
      0.5,
      -1.0,
      1.5,
      1.75
    ],
    "independent_reference_cases": 360,
    "batch_split_comparisons": 6
  },
  "checks": {
    "hand_asymmetric_golden_output": true,
    "hand_final_oldest_first_history": true,
    "unit_impulse_preserves_tap_order": true,
    "explicit_zero_flush_matches_full_convolution": true,
    "independent_rational_scatter_reference": true,
    "all_batch_splits_carry_input_history": true,
    "reset_at_split_exposes_regression": true,
    "hand_nonzero_initial_history": true,
    "empty_batch_preserves_history": true,
    "one_tap_has_no_history": true,
    "zero_taps_are_allowed_nonempty": true,
    "constant_input_settles_to_tap_sum": true,
    "finite_fixture_linearity": true,
    "zero_prefix_delays_output": true,
    "invalid_coefficients_rejected": true,
    "invalid_sample_history_and_unit_contract": true,
    "overflow_and_late_invalid_do_not_mutate_inputs": true,
    "every_prefix_has_no_future_dependency": true
  },
  "limitations": [
    "Own input-history representation is not SciPy direct-II zi/zf; no SciPy/NumPy execution.",
    "No resampling, continuous-time integration, dt multiplier, frequency response design or noise/performance claim.",
    "Finite input/products/fsum required; overflow rejected, not universally prevented; rounding and underflow remain possible.",
    "Finite golden/rational fixtures are evidence, not proof for all floating-point arrays or device validation."
  ]
}

مرجع مستقل با Fraction سهم هر ورودی و ضریب را در خانهٔ مجموع اندیس‌هایشان پخش می‌کند؛ از حافظه و حلقهٔ جمع‌آوری تابع اصلی استفاده نمی‌کند. ۳۶۰ دادهٔ محدود با مقدارها و ضرایب قابل نمایش دقیقِ انتخابی مقایسه شده‌اند. این‌ها یک گروه از هجده گروه بررسی‌اند. پاسخ ضربه نیز ترتیب ضرایب را آشکار می‌کند: یک ولت و سپس صفرها، خروجی ۰٫۵، ۰٫۲۵، منفی ۰٫۱۲۵ و صفر می‌دهد.

تمام شش محل برش ورودی اصلی بررسی می‌شوند. اگر تاریخچه منتقل شود، خروجی یکجا و تکه‌ای برابر است؛ اگر بعد از دو نمونه بازنشانی کنیم، خروجی سوم منفی یک می‌شود و پاسخ درستِ منفی ۱٫۲۵ را از دست می‌دهیم. این شکست آموزشی با انتظارِ متفاوت آزموده می‌شود و خطای حل‌نشده نیست. بستهٔ خالی باید تاریخچه را حفظ کند؛ نمونه‌های ابتدای بستهٔ بعدی هنوز از آن استفاده می‌کنند.

آزمون دامنهٔ FFT به تفسیر دامنه می‌پردازد و آزمون عددی فیلتر مرتبهٔ اول رفتار دیگری دارد. اینجا نگهداری آزمون FIR مهم است: پس از تغییر ضرایب یا سیاست آغاز، پاسخ طلایی را فقط برای سبز شدن آزمون عوض نکنیم. ابتدا قرارداد تازه، جواب دستی و مرجع مستقل را تطبیق دهیم و دلیل تغییر را ثبت کنیم.

۵. چهار پرسش پاسخ‌دار برای آزمون بازگشت

حفظ حافظه در مرز بسته‌ها و آزمون بازگشت
تصویر مفهومی: حفظ حافظه در مرز بسته‌ها و آزمون بازگشت؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: چرا ضرایب متقارن برای تنها آزمون ترتیب کافی نیستند؟ پاسخ: معکوس‌کردن آن‌ها همان فهرست را می‌سازد. ضرایب نامتقارن مثال و پاسخ ضربه، جابه‌جایی ضریب نمونهٔ جاری و گذشته را آشکار می‌کنند.

پرسش دوم: بازنشانی بعد از دو ورودی چه خطایی می‌سازد؟ پاسخ: سهم منفی ۰٫۱۲۵ ضربدر دو ولت حذف می‌شود؛ خروجی سوم منفی یک به‌جای منفی ۱٫۲۵ ولت خواهد بود. حافظهٔ قسمت اول باید منتقل شود.

پرسش سوم: پنج ورودی چرا هفت خروجی خودکار نمی‌دهند؟ پاسخ: قرارداد تابع N خروجی است. با دو صفر و حافظهٔ نهایی، دو مقدار منفی ۰٫۲۵ و منفی ۰٫۱۲۵ اضافه و پاسخ full هفت‌عضوی ساخته می‌شود.

پرسش چهارم: تغییر مجاز ضرایب یعنی پاسخ طلایی قبلی را پاک کنیم؟ پاسخ: ابتدا انتظار جدید را با محاسبهٔ دستی و مرجع مستقل تأیید کنیم و تغییر نیازمندی را ثبت کنیم. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ هجده بررسی این کد، طراحی فیلتر یا عملکرد دستگاه واقعی را تأیید نمی‌کند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.