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

مسئله، واحدها و فرض میان دو نمونه را روشن کنیم

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

زمان‌های آزمایش [0,0.25,1,2.5,4] ثانیه و ولتاژها [2,4,1,-1,3] ولت هستند. پنج نمونه، چهار بازه با طول‌های متفاوت می‌سازند. اگر فقط همین نقاط معلوم باشند، شکل دقیق سیگنال در فاصلهٔ بین آن‌ها مشخص نیست. فرض ما خط مستقیم در هر بازه است؛ در این مدل، سهم هر بازه دقیقاً مساحت علامت‌دار ذوزنقه است. برای سیگنال پیوستهٔ ناشناخته، این یک تقریب مبتنی بر فرض است.

فرمول بازه چنین است: A_i=(t_(i+1)-t_i)*(v_i+v_(i+1))/2. زمان برحسب ثانیه و ولتاژ برحسب ولت است؛ بنابراین واحد A در اینجا ولت‌ثانیه است، نه آمپر و نه ژول. نام متغیر «area» نیز نباید باعث شود آن را مساحت هندسیِ همواره مثبت بدانیم. علامت ولتاژ در جمع حفظ می‌شود.

مرجع رسمی trapz MathWorks فرمول فاصله‌های نامنظم را در بخش Trapezoidal Method ارائه می‌کند. در این مطلب، پیاده‌سازی مستقلِ کتابخانهٔ استاندارد پایتون را اجرا می‌کنیم؛ اجرای MATLAB ادعا نمی‌شود. برای قرارداد اتصال دو گره و مرز مدل نیز آزمون درون‌یابی خطی کالیبراسیون مکمل مناسبی است.

جواب دستی و میانگین زمانی را پیش از کدنویسی بسازیم

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

در بازهٔ اول، عرض ۰٫۲۵ ثانیه و میانگین دو ولتاژ سه ولت است؛ سهم آن ۰٫۷۵ ولت‌ثانیه می‌شود. بازهٔ دوم، عرض ۰٫۷۵ و میانگین ۲٫۵ دارد؛ سهم آن ۱٫۸۷۵ است. در بازهٔ سوم، دو انتها یک و منفی یک‌اند؛ میانگین صفر و سهم علامت‌دار صفر است. بازهٔ چهارم، عرض ۱٫۵ و میانگین یک دارد؛ سهم آن ۱٫۵ ولت‌ثانیه است.

جمع سهم‌ها 33/8=4.125 ولت‌ثانیه است. مدت کل چهار ثانیه است؛ پس میانگین زمانی مدل خطی‌تکه‌ای 33/32=1.03125 ولت می‌شود. میانگین حسابی پنج ولتاژ، ۱٫۸ ولت است. دومی به هر نمونه وزن یکسان می‌دهد؛ اولی زمان حضور میان نقاط و سهم دو انتهای هر بازه را لحاظ می‌کند. حتی در شبکهٔ زمانی یکنواخت نیز وزن‌های انتهایی ذوزنقه با میانگین سادهٔ تمام نمونه‌ها عموماً یکسان نیستند.

صفرشدن یک سهم، به معنی نبود تغییر یا نبود بخش منفی نیست. برای اتصال خطی منفی یک به مثبت یک ولت در دو ثانیه، انتگرال علامت‌دار صفر است، اما انتگرال قدرمطلق همان مدل یک ولت‌ثانیه است. برای محاسبهٔ دومی باید شکست در عبور از صفر را لحاظ کنیم؛ صرف گرفتن قدرمطلق انتگرال، آن را به دست نمی‌دهد.

قرارداد زمان صعودی و محاسبهٔ متناهی را پیاده کنیم

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

تابع دو list یا tuple دقیق، با طول برابر و بین دو تا ۴۰۹۶ نمونه می‌گیرد. هر مقدار باید int یا float دقیق و متناهی باشد؛ bool، عدد مختلط، رشته، NaN و بی‌نهایت رد می‌شوند. زمان نسبی باید نامنفی و پس از تبدیل به float اکیداً صعودی باشد. زمان تکراری یا عقب‌رونده را مرتب‌سازی پنهان نمی‌کنیم؛ اشکال ترتیب داده باید آشکار بماند.

این قرارداد از رابط عمومی numpy.trapezoid محدودتر است. مرجع NumPy می‌گوید xها به ترتیب ورودی استفاده می‌شوند و خودکار مرتب نمی‌شوند؛ حرکت معکوس نیز می‌تواند انتگرال معکوس بدهد. ما ترتیب زمانی ثبت نمونه را می‌خواهیم و کاهش زمان را خطا می‌گیریم. کتابخانهٔ NumPy در اجرای کد حاضر فراخوانی نمی‌شود.

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

"""Authored nonuniform voltage/time fixture; piecewise-linear model only.

Standard-library implementation, exact-rational polynomial oracle, no hardware,
MATLAB/NumPy call, acquisition, power measurement or energy calculation.
"""
from fractions import Fraction
import json
import math
import platform


def finite_number(value):
    if type(value) not in (int, float):
        raise TypeError('samples must be exact int/float, not bool')
    try:
        converted = float(value)
    except OverflowError as error:
        raise ValueError('number cannot be represented as a finite float') from error
    if not math.isfinite(converted):
        raise ValueError('samples must be finite')
    return converted


def trapezoid_report(times_s, voltage_v):
    if type(times_s) not in (list, tuple) or type(voltage_v) not in (list, tuple):
        raise TypeError('supply exact lists or tuples')
    if not 2 <= len(times_s) <= 4096 or len(times_s) != len(voltage_v):
        raise ValueError('equal lengths, with 2..4096 samples, are required')
    times = [finite_number(x) for x in times_s]
    volts = [finite_number(x) for x in voltage_v]
    if any(x < 0 for x in times) or any(b <= a for a, b in zip(times, times[1:])):
        raise ValueError('relative seconds must be nonnegative and strictly increasing after float conversion')
    intervals = []
    for left, right, vleft, vright in zip(times, times[1:], volts, volts[1:]):
        width = right - left
        average = vleft / 2.0 + vright / 2.0  # Avoid needless same-sign sum overflow.
        area = width * average
        if not math.isfinite(width) or not math.isfinite(average) or not math.isfinite(area):
            raise ArithmeticError('nonfinite interval arithmetic')
        intervals.append({'width_s': width, 'signed_area_v_s': area})
    try:
        total = math.fsum(x['signed_area_v_s'] for x in intervals)
    except OverflowError as error:
        raise ArithmeticError('summed area exceeded finite float range') from error
    duration = times[-1] - times[0]
    mean = total / duration
    if not all(math.isfinite(x) for x in (total, duration, mean)):
        raise ArithmeticError('nonfinite total, duration or time-weighted mean')
    return {'sample_count': len(times), 'intervals': intervals,
            'duration_s': duration, 'signed_integral_v_s': total,
            'piecewise_linear_time_mean_v': mean}


def polynomial_antiderivative_oracle(times, volts):
    """Integrate exact a*t+b per segment, rather than trapezoid arithmetic."""
    areas = []
    for a, b, va, vb in zip(times, times[1:], volts, volts[1:]):
        slope = (vb - va) / (b - a)
        intercept = va - slope * a
        primitive = lambda t: slope * t * t / 2 + intercept * t
        areas.append(primitive(b) - primitive(a))
    return areas, sum(areas, Fraction(0))


def fails(error, function, *args):
    try:
        function(*args)
    except error:
        return True
    return False


def run_example():
    times = [0, 0.25, 1, 2.5, 4]
    volts = [2, 4, 1, -1, 3]
    times_before, volts_before = times.copy(), volts.copy()
    observed = trapezoid_report(times, volts)
    exact_t = [Fraction(0), Fraction(1, 4), Fraction(1), Fraction(5, 2), Fraction(4)]
    exact_v = list(map(Fraction, volts))
    oracle_areas, oracle_total = polynomial_antiderivative_oracle(exact_t, exact_v)
    linear = trapezoid_report(times, [2 * t + 1 for t in times])
    negative_constant = trapezoid_report(times, [-2] * 5)
    sign_flip = trapezoid_report(times, [-v for v in volts])
    offset = trapezoid_report(times, [v + 3 for v in volts])
    time_shift = trapezoid_report([t + 10 for t in times], volts)
    refined = trapezoid_report([0, .25, .625, 1, 2.5, 4], [2, 4, 2.5, 1, -1, 3])
    times_ms = [t * 1000 for t in times]
    corrected = trapezoid_report([t / 1000 for t in times_ms], volts)
    wrongly_labeled_seconds = trapezoid_report(times_ms, volts)
    quadratic = trapezoid_report([0, 1, 2, 3], [0, 1, 4, 9])
    signed_cancellation = trapezoid_report([0, 2], [-1, 1])
    split_absolute = trapezoid_report([0, 1, 2], [1, 0, 1])
    checks = {
        'four_hand_segment_areas_match_nonuniform_widths':
            observed['intervals'] == [{'width_s': .25, 'signed_area_v_s': .75},
                                      {'width_s': .75, 'signed_area_v_s': 1.875},
                                      {'width_s': 1.5, 'signed_area_v_s': 0.0},
                                      {'width_s': 1.5, 'signed_area_v_s': 1.5}],
        'signed_total_is_33_over8_voltseconds': observed['signed_integral_v_s'] == 4.125,
        'four_second_duration_gives33_over32_volt_time_mean':
            observed['duration_s'] == 4.0 and observed['piecewise_linear_time_mean_v'] == 1.03125,
        'ordinary_five_sample_mean_is_not_time_mean':
            math.fsum(volts) / len(volts) == 1.8 and observed['piecewise_linear_time_mean_v'] != 1.8,
        'independent_exact_polynomial_antiderivatives_match_every_area':
            oracle_areas == [Fraction(3, 4), Fraction(15, 8), Fraction(0), Fraction(3, 2)] and
            oracle_total == Fraction(33, 8) and
            [float(x) for x in oracle_areas] == [x['signed_area_v_s'] for x in observed['intervals']],
        'analytic_linear_voltage2t_plus1_integrates_to20_with_mean5':
            linear['signed_integral_v_s'] == 20.0 and linear['piecewise_linear_time_mean_v'] == 5.0,
        'negative_constant_has_negative_signed_area_and_unchanged_mean':
            negative_constant['signed_integral_v_s'] == -8.0 and negative_constant['piecewise_linear_time_mean_v'] == -2.0,
        'negating_voltage_negates_integral_and_time_mean':
            sign_flip['signed_integral_v_s'] == -4.125 and sign_flip['piecewise_linear_time_mean_v'] == -1.03125,
        'adding3volts_adds12voltseconds_and3volts_to_mean':
            offset['signed_integral_v_s'] == 16.125 and offset['piecewise_linear_time_mean_v'] == 4.03125,
        'shifting_all_times_by10seconds_preserves_duration_area_and_mean': time_shift == observed,
        'inserting_exact_collinear_midpoint_preserves_integral':
            refined['signed_integral_v_s'] == observed['signed_integral_v_s'] and
            refined['piecewise_linear_time_mean_v'] == observed['piecewise_linear_time_mean_v'],
        'explicit_ms_to_seconds_conversion_restores_units_but_mislabeled_input_scales_area1000':
            corrected == observed and wrongly_labeled_seconds['signed_integral_v_s'] == 4125.0 and
            wrongly_labeled_seconds['piecewise_linear_time_mean_v'] == 1.03125,
        'quadratic_samples_have_model_error_against_exact_integral9':
            quadratic['signed_integral_v_s'] == 9.5 and quadratic['signed_integral_v_s'] - 9.0 == .5,
        'zero_signed_area_is_not_absolute_voltage_area':
            signed_cancellation['signed_integral_v_s'] == 0.0 and split_absolute['signed_integral_v_s'] == 1.0,
        'minimum_two_tuple_samples_max4096_and_inputs_remain_unchanged':
            trapezoid_report((0, 1), (2, 4))['signed_integral_v_s'] == 3.0 and
            trapezoid_report(list(range(4096)), [1] * 4096)['signed_integral_v_s'] == 4095.0 and
            times == times_before and volts == volts_before,
        'invalid_containers_shapes_numeric_types_nonfinite_values_rejected':
            fails(TypeError, trapezoid_report, iter([0, 1]), [1, 2]) and
            fails(TypeError, trapezoid_report, [0, 1], '12') and
            all(fails(ValueError, trapezoid_report, t, v) for t, v in (([], []), ([0], [1]), ([0, 1], [1]), (list(range(4097)), [1] * 4097))) and
            all(fails(TypeError, trapezoid_report, [0, 1], [1, bad]) for bad in (True, '2', None, 1 + 0j)) and
            all(fails(ValueError, trapezoid_report, [0, 1], [1, bad]) for bad in (math.nan, math.inf, -math.inf, 10**10000)),
        'duplicate_descending_negative_and_float_collapsed_times_rejected':
            all(fails(ValueError, trapezoid_report, t, [1, 2]) for t in ([0, 0], [1, 0], [-1, 1], [2**54, 2**54 + 1])) and
            fails(TypeError, trapezoid_report, [0, True], [1, 2]) and
            fails(ValueError, trapezoid_report, [0, math.inf], [1, 2]),
        'half_before_add_avoids_needless_overflow_and_nonfinite_areas_or_sums_fail':
            trapezoid_report([0, 1], [1e308, 1e308])['signed_integral_v_s'] == 1e308 and
            fails(ArithmeticError, trapezoid_report, [0, 2], [1e308, 1e308]) and
            fails(ArithmeticError, trapezoid_report, [0, 1, 2], [1e308, 1e308, 1e308]),
    }
    if len(checks) != 18 or not all(type(value) is bool and value for value in checks.values()):
        raise AssertionError(checks)
    return {'python': platform.python_version(),
            'scope': 'Authored synthetic volts and relative seconds; piecewise-linear signed integral, not actual sensor acquisition, true continuous signal, power or energy.',
            'times_s': times, 'voltage_v': volts, 'observed': observed,
            'exact_oracle_segment_v_s': list(map(str, oracle_areas)),
            'exact_oracle_total_v_s': str(oracle_total),
            'exact_oracle_time_mean_v': str(oracle_total / (exact_t[-1] - exact_t[0])),
            'ordinary_sample_mean_v': 1.8,
            'quadratic_model_area_v_s': quadratic['signed_integral_v_s'],
            'quadratic_analytic_area_v_s': 9.0,
            'checks': checks}


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

مرجع مستقل و آزمون‌هایی فراتر از مقایسه با خود تابع

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

مرجع، زمان‌های کوچک آزمایش را مستقیماً با صورت و مخرج صحیح می‌سازد. سپس در هر بازه، شیب و عرض از مبدأ خط a*t+b را پیدا می‌کند و مقدار تابع اولیهٔ a*t²/2+b*t را در دو انتها کم می‌کند. مسیر محاسبه با میانگین دو انتها متفاوت است؛ چهار سهم کسری و جمع آن با جواب دستی برابرند. مستندات Fraction نیز هشدار می‌دهد که ساخت کسر از float، مقدار باینری همان float را حفظ می‌کند؛ این کار لزوماً عدد اعشاریِ منظور ما را بازیابی نمی‌کند.

آزمون‌های دیگر، تابع خطی معلوم، ولتاژ ثابت منفی، وارون‌کردن علامت، افزودن offset ولتاژ و جابه‌جایی مبدأ زمان را پوشش می‌دهند. افزودن نقطهٔ هم‌خط در زمان ۰٫۶۲۵ ثانیه با ولتاژ ۲٫۵، مساحت این مدل را تغییر نمی‌دهد. برای مدل ساختگی v(t)=(1 V/s²)*t² در زمان‌های صفر، یک، دو و سه ثانیه، روش مقدار ۹٫۵ می‌دهد، درحالی‌که انتگرال تحلیلی نه است؛ خطای ۰٫۵ نشان می‌دهد اجرای درست الگوریتم با دقیق‌بودن مدل پیوسته یکی نیست.

تبدیل واحد هم آزمون مستقل دارد: اگر اعداد میلی‌ثانیه را اشتباهاً ثانیه معرفی کنیم، انتگرال هزار برابر می‌شود؛ بااین‌حال تقسیم بر مدت نیز هزار برابر شده و میانگین عددی همان می‌ماند. پس درست‌بودن ظاهری میانگین، صحت واحد زمان را ثابت نمی‌کند. خروجی زیر ۱۸ بررسی نام‌دار در Python 3.14.5 است؛ برای چارچوب ثبت داده و نسخه‌ها، آزمایش تکرارپذیر سیگنال در پایتون را ببینید.

{
  "python": "3.14.5",
  "scope": "Authored synthetic volts and relative seconds; piecewise-linear signed integral, not actual sensor acquisition, true continuous signal, power or energy.",
  "times_s": [
    0,
    0.25,
    1,
    2.5,
    4
  ],
  "voltage_v": [
    2,
    4,
    1,
    -1,
    3
  ],
  "observed": {
    "sample_count": 5,
    "intervals": [
      {
        "width_s": 0.25,
        "signed_area_v_s": 0.75
      },
      {
        "width_s": 0.75,
        "signed_area_v_s": 1.875
      },
      {
        "width_s": 1.5,
        "signed_area_v_s": 0.0
      },
      {
        "width_s": 1.5,
        "signed_area_v_s": 1.5
      }
    ],
    "duration_s": 4.0,
    "signed_integral_v_s": 4.125,
    "piecewise_linear_time_mean_v": 1.03125
  },
  "exact_oracle_segment_v_s": [
    "3/4",
    "15/8",
    "0",
    "3/2"
  ],
  "exact_oracle_total_v_s": "33/8",
  "exact_oracle_time_mean_v": "33/32",
  "ordinary_sample_mean_v": 1.8,
  "quadratic_model_area_v_s": 9.5,
  "quadratic_analytic_area_v_s": 9.0,
  "checks": {
    "four_hand_segment_areas_match_nonuniform_widths": true,
    "signed_total_is_33_over8_voltseconds": true,
    "four_second_duration_gives33_over32_volt_time_mean": true,
    "ordinary_five_sample_mean_is_not_time_mean": true,
    "independent_exact_polynomial_antiderivatives_match_every_area": true,
    "analytic_linear_voltage2t_plus1_integrates_to20_with_mean5": true,
    "negative_constant_has_negative_signed_area_and_unchanged_mean": true,
    "negating_voltage_negates_integral_and_time_mean": true,
    "adding3volts_adds12voltseconds_and3volts_to_mean": true,
    "shifting_all_times_by10seconds_preserves_duration_area_and_mean": true,
    "inserting_exact_collinear_midpoint_preserves_integral": true,
    "explicit_ms_to_seconds_conversion_restores_units_but_mislabeled_input_scales_area1000": true,
    "quadratic_samples_have_model_error_against_exact_integral9": true,
    "zero_signed_area_is_not_absolute_voltage_area": true,
    "minimum_two_tuple_samples_max4096_and_inputs_remain_unchanged": true,
    "invalid_containers_shapes_numeric_types_nonfinite_values_rejected": true,
    "duplicate_descending_negative_and_float_collapsed_times_rejected": true,
    "half_before_add_avoids_needless_overflow_and_nonfinite_areas_or_sums_fail": true
  }
}

محدودیت‌ها و چهار تمرین با پاسخ

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

عدد متناهی الزاماً نمایش دقیق ورودی نیست. برای مثال، دو زمان صحیح بزرگ و متوالی ممکن است پس از تبدیل به float یکسان شوند؛ قرارداد صعودی آن مورد را رد می‌کند. نویز حسگر، کالیبراسیون، دادهٔ گمشده و شکل واقعی بین نمونه‌ها در این آزمایش برآورد نشده‌اند. آزمون واحد به صحت قرارداد کمک می‌کند و جای تحلیل خطای اندازه‌گیری را نمی‌گیرد.

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

تمرین دو: اگر سه ولت به همهٔ نمونه‌ها اضافه کنیم چه می‌شود؟ پاسخ: در چهار ثانیه، ۱۲ ولت‌ثانیه به مساحت اضافه می‌شود؛ انتگرال ۱۶٫۱۲۵ و میانگین زمانی ۴٫۰۳۱۲۵ ولت خواهد بود.

تمرین سه: آیا ۴٫۱۲۵، انرژی مصرفی است؟ پاسخ: خیر؛ واحد آن ولت‌ثانیه است. انرژی به انتگرال توان نیاز دارد و رابطهٔ ولتاژ با جریان یا مدل بار باید مشخص باشد. چنین داده‌ای در مثال نداریم.

تمرین چهار: آیا افزودن هر نقطهٔ تازه نتیجه را ثابت نگه می‌دارد؟ پاسخ: خیر؛ آزمون ما نقطهٔ دقیقاً هم‌خط را اضافه می‌کند و همان مدل قبلی را حفظ می‌کند. نقطهٔ خارج از خط، مدل بین نمونه‌ها و احتمالاً انتگرال را تغییر می‌دهد.

منابع در ۲۴ مهر ۱۴۰۵ خوانده شده‌اند. مستندات Python 3.14.8، NumPy 2.4 و MATLAB R2026b مرجع بوده‌اند؛ اجرای واقعی فقط Python 3.14.5 استاندارد است. تاریخ نخستین انتشار صفحات مستندات مشخص نشده و مطلب به‌عنوان خبر تازه معرفی نمی‌شود.

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

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