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

۱. دو کانالِ ناهم‌زمان، یک مبنای زمان

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

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

ولتاژ در زمانِ صفر، ۱۲۰۰۰ میلی‌ولت و در زمانِ ۶۰، ۲۴۰۰۰ میلی‌ولت است. جریان در زمان‌های ۲۰، ۵۰، ۱۱۰ و ۱۲۰، به‌ترتیب ۵۰۰، ۱۰۰۰، منفیِ ۲۵۰ و ۹۰۰۰ میلی‌آمپر است. پنجرهٔ بررسی از صفر تا ۱۲۰ میلی‌ثانیه، با پایانِ خارج از بازه است. این داده‌ها ساختهٔ آموزش‌اند؛ نه خروجیِ اندازه‌گیریِ موتور و نه مشخصاتِ یک محصول.

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

در جفت‌کردنِ سخت‌گیرانهٔ کانال‌های حسگر، کلیدِ مشترک و طول بررسی می‌شد و زمانِ فیزیکی تعریف نشده بود. این مسئله متفاوت است: شمارِ نمونه‌ها می‌تواند فرق کند و کلیدها عمداً هم‌زمان نیستند. مرتب‌کردن یا کوتاه‌کردنِ دو ستون، جایِ تعریفِ حالتِ هر کانال در هر زمان را نمی‌گیرد.

۲. آخرین نمونه و مهلتِ اعتبارِ آن

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

در هر زمان، تازه‌ترین رکورد با زمانِ کوچک‌تر یا مساویِ آن را انتخاب می‌کنیم. مستنداتِ merge_asof در pandas 3.0.6، جهتِ backward را از forward و nearest جدا می‌کند و مرتب‌بودنِ کلید و تحملِ فاصله را توضیح می‌دهد. اینجا pandas اجرا نمی‌شود؛ اصلِ انتخابِ رکوردِ قبلی را با کتابخانهٔ استاندارد پیاده می‌کنیم.

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

قبل از اولین نمونه، مقدار نامعلوم است. بعد از پایانِ اعتبار نیز نمونه کهنه است؛ هیچ‌کدام را صفر نمی‌گذاریم. در زمانِ دقیقِ نمونهٔ تازه، همان نمونه انتخاب می‌شود؛ در پایانِ اعتبار، اگر جایگزین نداریم، مقدار کهنه است. اصطلاحِ previous در مرجعِ interp1d در SciPy 1.18.0 نیز به مقدارِ قبلی اشاره دارد. آن کلاس legacy است؛ این استناد تعریفِ مفهوم است، نه توصیهٔ اجرای آن یا ادعای تقلیدِ همهٔ قواعدِ API.

۳. انرژیِ بخشِ پوشش‌داده‌شده را حساب کنیم

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

در فاصله‌ای که هر دو مقدار معتبر و ثابت‌اند، سهمِ انرژی برابرِ ولتاژ ضرب‌در جریان ضرب‌در مدت است. یک میلی‌ولت ضرب‌در یک میلی‌آمپر، یک میکرووات است؛ ضرب در یک میلی‌ثانیه، یک نانوژول می‌دهد. پس می‌توان سهم‌ها را با عددِ صحیحِ نانوژول جمع کرد و برای ژول، بر یک‌میلیارد تقسیم کرد. این دقتِ حسابی، فرضِ ثابت‌بودنِ سیگنال را دقیق نمی‌کند.

از صفر تا ۲۰، جریان نداریم. از ۲۰ تا ۵۰، توانِ مدل شش وات و سهمِ ۳۰ میلی‌ثانیه، ۰٫۱۸ ژول است. از ۵۰ تا ۶۰، ۱۲ وات در ده میلی‌ثانیه، ۰٫۱۲ ژول می‌دهد. از ۶۰ تا ۹۰، ۲۴ وات در ۳۰ میلی‌ثانیه، ۰٫۷۲ ژول داریم. در ۹۰، جریانِ زمانِ ۵۰ کهنه می‌شود؛ تا ۱۱۰، سهمِ انرژی را نامعلوم نگه می‌داریم.

از ۱۱۰ تا ۱۲۰، جریانِ منفیِ ۰٫۲۵ آمپر در ۲۴ ولت، توانِ منفیِ شش وات و سهمِ منفیِ ۰٫۰۶ ژول دارد. جمعِ سهم‌های معلوم، ۰٫۹۶ ژول است. فقط ۸۰ میلی‌ثانیه پوشش داریم؛ ۲۰ میلی‌ثانیه ابتدا نامعلوم و ۲۰ میلی‌ثانیه کهنه است. این جمع را انرژیِ تمامِ پنجره یا میانگینِ توانِ کلِ ۱۲۰ میلی‌ثانیه معرفی نمی‌کنیم.

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

۴. کد کامل و مرجعِ مستقلِ میلی‌ثانیه‌ها

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

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

فایلِ asof_energy.py مستقل و بدونِ وابستگیِ خارجی است. هر کانال، tuple دقیق با حداکثر ۶۴ رکوردِ دوعضوی است؛ زمان‌ها int دقیقِ صعودی از صفر تا ده‌هزار میلی‌ثانیه‌اند. ولتاژ از صفر تا ۶۰۰۰۰ میلی‌ولت و جریان از منفیِ ۲۰۰۰۰ تا ۲۰۰۰۰ میلی‌آمپر پذیرفته می‌شود. زمانِ تکراری، ترتیبِ کاهشی، bool و تبدیلِ خودکار رد می‌شوند. این کران‌ها برای آزمایش‌اند، نه رده‌بندیِ تجهیزات.

"""Synthetic asynchronous DC logs: covered sample-and-hold energy, not a meter."""
from bisect import bisect_right
from fractions import Fraction
from itertools import combinations, product
import json
import platform


def checked_events(events, minimum, maximum):
    if type(events) is not tuple:
        raise TypeError('Exact tuple of events required')
    if len(events) > 64:
        raise ValueError('At most64 events per channel in this teaching profile')
    previous = -1
    for row in events:
        if type(row) is not tuple or len(row) != 2:
            raise TypeError('Exact(time_ms,value) tuple required')
        time, value = row
        if type(time) is not int or type(value) is not int:
            raise TypeError('Exact int time and value; no bool or subclasses')
        if not 0 <= time <= 10000 or time <= previous:
            raise ValueError('Strictly increasing timestamps0..10000ms required')
        if not minimum <= value <= maximum:
            raise ValueError('Value outside declared physical-unit profile')
        previous = time


def held_state(events, times, at_ms, max_age_ms):
    """Internal validated data: latest past-or-present event, never future fill."""
    index = bisect_right(times, at_ms) - 1
    if index < 0:
        return 'unknown', None, None
    stamp, value = events[index]
    if at_ms >= stamp + max_age_ms:
        return 'stale', stamp, None
    return 'fresh', stamp, value


def energy_report(voltage, current, start_ms, end_ms, voltage_age_ms, current_age_ms):
    """Assume a common event-time clock and a constant held value, not accuracy."""
    checked_events(voltage, 0, 60000)
    checked_events(current, -20000, 20000)
    if any(type(v) is not int for v in (start_ms, end_ms, voltage_age_ms, current_age_ms)):
        raise TypeError('Exact integer window and age limits required')
    if not 0 <= start_ms < end_ms <= 10000:
        raise ValueError('Window must satisfy0<=start<end<=10000ms')
    if not all(1 <= age <= 10000 for age in (voltage_age_ms, current_age_ms)):
        raise ValueError('Positive age limits1..10000ms required')
    breaks = {start_ms, end_ms}
    for events, age in ((voltage, voltage_age_ms), (current, current_age_ms)):
        for index, (stamp, _) in enumerate(events):
            next_stamp = events[index + 1][0] if index + 1 < len(events) else end_ms
            expiry = min(stamp + age, next_stamp)
            if start_ms < stamp < end_ms:
                breaks.add(stamp)
            if start_ms < expiry < end_ms:
                breaks.add(expiry)
    times_v, times_i = tuple(t for t, _ in voltage), tuple(t for t, _ in current)
    points = sorted(breaks)
    rows = []
    durations = {'covered': 0, 'initial_unknown': 0, 'stale': 0}
    positive_nj = negative_nj = 0
    for left, right in zip(points, points[1:]):
        vstate, vtime, mv = held_state(voltage, times_v, left, voltage_age_ms)
        istate, itime, ma = held_state(current, times_i, left, current_age_ms)
        if vstate == istate == 'fresh':
            status = 'covered'
            contribution = mv * ma * (right - left)
            positive_nj += max(contribution, 0)
            negative_nj += min(contribution, 0)
        else:
            status = 'initial_unknown' if 'unknown' in (vstate, istate) else 'stale'
            contribution = None
        durations[status] += right - left
        rows.append({'interval_ms': [left, right], 'status': status,
                     'voltage_state': vstate, 'current_state': istate,
                     'voltage_source_time_ms': vtime, 'current_source_time_ms': itime,
                     'voltage_mV': mv, 'current_mA': ma,
                     'covered_energy_nJ': contribution})
    net_nj = positive_nj + negative_nj
    return {'window_ms': [start_ms, end_ms], 'intervals': rows,
            'durations_ms': durations, 'uncovered_ms': end_ms - start_ms - durations['covered'],
            'positive_covered_energy_nJ': positive_nj,
            'negative_covered_energy_nJ': negative_nj,
            'net_covered_energy_nJ': net_nj,
            'net_covered_energy_J_exact': str(Fraction(net_nj, 1000000000)),
            'model_covers_entire_window': durations['covered'] == end_ms - start_ms}


def millisecond_cell_oracle(voltage, current, start, end, vage, iage):
    """Already validated integer-grid fixtures; no bisect, breaks or held helper.

    Every event/expiry has an integer-ms boundary. State is constant inside each
    unit cell, so midpoint enumeration integrates the chosen step model exactly.
    This does not infer an unknown continuous physical waveform.
    """
    duration = {'covered': 0, 'initial_unknown': 0, 'stale': 0}
    positive = negative = 0
    for tick in range(start, end):
        midpoint = Fraction(2 * tick + 1, 2)
        values, states = [], []
        for channel, age in ((voltage, vage), (current, iage)):
            candidates = [row for row in channel if row[0] <= midpoint]
            if not candidates:
                states.append('unknown'); values.append(None)
            else:
                chosen = max(candidates, key=lambda row: row[0])
                if midpoint - chosen[0] >= age:
                    states.append('stale'); values.append(None)
                else:
                    states.append('fresh'); values.append(chosen[1])
        if states == ['fresh', 'fresh']:
            state = 'covered'; energy = values[0] * values[1]
            positive += max(energy, 0); negative += min(energy, 0)
        else:
            state = 'initial_unknown' if 'unknown' in states else 'stale'
        duration[state] += 1
    return duration, positive, negative


def summary(report):
    return report['durations_ms'], report['positive_covered_energy_nJ'], report['negative_covered_energy_nJ']


def rejects(error, *args):
    try:
        energy_report(*args)
    except error:
        return True
    return False


def run():
    voltage = ((0, 12000), (60, 24000))
    current = ((20, 500), (50, 1000), (110, -250), (120, 9000))
    result = energy_report(voltage, current, 0, 120, 80, 40)
    hand_rows = [([0, 20], 'initial_unknown', None), ([20, 50], 'covered', 180000000),
                 ([50, 60], 'covered', 120000000), ([60, 90], 'covered', 720000000),
                 ([90, 110], 'stale', None), ([110, 120], 'covered', -60000000)]
    checks = {}
    checks['literal_interval_contributions_and_signed_known_energy_24_over25_J'] = (
        [(r['interval_ms'], r['status'], r['covered_energy_nJ']) for r in result['intervals']] == hand_rows
        and result['net_covered_energy_J_exact'] == '24/25'
        and result['durations_ms'] == {'covered': 80, 'initial_unknown': 20, 'stale': 20}
        and result['uncovered_ms'] == 40 and result['model_covers_entire_window'] is False)
    checks['end_cutoff_sample_has_zero_duration_no_future_backfill'] = (
        energy_report(voltage, current[:-1], 0, 120, 80, 40) == result
        and result['intervals'][0]['current_mA'] is None
        and energy_report(voltage, current + ((200, -10000),), 0, 120, 80, 40) == result)
    zero = energy_report(((0, 12000),), ((0, 0),), 0, 10, 20, 20)
    absent = energy_report(((0, 12000),), (), 0, 10, 20, 20)
    checks['observed_zero_current_is_covered_zero_not_missing_zero_substitution'] = (
        zero['durations_ms']['covered'] == 10 and zero['net_covered_energy_nJ'] == 0
        and absent['durations_ms']['covered'] == 0 and absent['uncovered_ms'] == 10
        and absent['intervals'][0]['covered_energy_nJ'] is None)
    expiry = energy_report(((0, 12000),), ((0, 1000),), 0, 10, 100, 4)
    checks['expiry_splits_interval_even_without_new_event_and_equality_is_stale'] = (
        expiry['durations_ms'] == {'covered': 4, 'initial_unknown': 0, 'stale': 6}
        and expiry['net_covered_energy_J_exact'] == '6/125'
        and energy_report(((0, 12000),), ((0, 1000),), 4, 5, 100, 4)['durations_ms']['covered'] == 0)
    renewed = energy_report(((0, 12000),), ((0, 1000), (4, 0)), 0, 8, 100, 4)
    checks['event_on_expiry_replaces_old_sample_at_left_boundary'] = (
        renewed['durations_ms']['covered'] == 8 and renewed['net_covered_energy_nJ'] == 48000000
        and renewed['intervals'][-1]['current_mA'] == 0)
    left = energy_report(voltage, current, 0, 60, 80, 40)
    right = energy_report(voltage, current, 60, 120, 80, 40)
    checks['split_horizon_additivity_uses_past_state_at_each_new_start'] = (
        left['net_covered_energy_nJ'] + right['net_covered_energy_nJ'] == result['net_covered_energy_nJ']
        and all(left['durations_ms'][k] + right['durations_ms'][k] == result['durations_ms'][k] for k in result['durations_ms'])
        and energy_report(voltage, current, 30, 70, 80, 40)['net_covered_energy_J_exact'] == '12/25')
    flipped = energy_report(voltage, tuple((t, -i) for t, i in current), 0, 120, 80, 40)
    checks['current_sign_reversal_negates_net_not_coverage_and_swaps_signed_parts'] = (
        flipped['net_covered_energy_nJ'] == -result['net_covered_energy_nJ']
        and flipped['durations_ms'] == result['durations_ms']
        and flipped['positive_covered_energy_nJ'] == -result['negative_covered_energy_nJ'])
    shifted = energy_report(tuple((t + 500, v) for t, v in voltage), tuple((t + 500, i) for t, i in current), 500, 620, 80, 40)
    stretched = energy_report(tuple((2*t, v) for t, v in voltage), tuple((2*t, i) for t, i in current), 0, 240, 160, 80)
    checks['time_origin_and_explicit_time_scale_have_different_unit_effects'] = (
        summary(shifted) == summary(result)
        and stretched['net_covered_energy_nJ'] == 2 * result['net_covered_energy_nJ']
        and stretched['durations_ms'] == {k: 2*v for k, v in result['durations_ms'].items()})
    doubled_v = energy_report(tuple((t, 2*v) for t, v in voltage), current, 0, 120, 80, 40)
    checks['voltage_scaling_doubles_energy_with_same_coverage'] = (
        doubled_v['net_covered_energy_nJ'] == 2 * result['net_covered_energy_nJ']
        and doubled_v['durations_ms'] == result['durations_ms'])
    profiles = [()]
    for size in (1, 2):
        for times in combinations(range(4), size):
            for values in product((0, 1, 2), repeat=size):
                profiles.append(tuple(zip(times, values)))
    cases = 0
    for va, ia in product(profiles, repeat=2):
        signed_i = tuple((t, i-1) for t, i in ia)
        vage, iage = cases % 3 + 1, (cases // 3) % 3 + 1
        actual = energy_report(va, signed_i, 0, 4, vage, iage)
        assert summary(actual) == millisecond_cell_oracle(va, signed_i, 0, 4, vage, iage)
        cases += 1
    checks['independent_rational_midpoint_fullscan_oracle_ONE_family'] = cases == len(profiles)**2
    checks['main_fixture_matches_independent_cell_reference'] = (
        summary(result) == millisecond_cell_oracle(voltage, current, 0, 120, 80, 40))
    class IntChild(int):
        pass
    class TupleChild(tuple):
        def __len__(self):
            raise AssertionError('Excluded tuple hooks must not execute')
    valid = (((0, 12000),), ((0, 1000),), 0, 10, 20, 20)
    malformed = [([ (0,12000) ], valid[1], 0, 10, 20, 20),
                 (TupleChild(valid[0]), valid[1], 0, 10, 20, 20),
                 (((0,True),), valid[1], 0, 10, 20, 20),
                 (((0,IntChild(1)),), valid[1], 0, 10, 20, 20),
                 (valid[0], valid[1], False, 10, 20, 20),
                 (valid[0], valid[1], 0, 10, 20.0, 20)]
    wrong_values = [(((0,1),(0,2)), valid[1], 0, 10, 20, 20),
                    (((2,1),(1,2)), valid[1], 0, 10, 20, 20),
                    (((0,-1),), valid[1], 0, 10, 20, 20),
                    (valid[0], ((0,20001),), 0, 10, 20, 20),
                    (valid[0], valid[1], 0, 0, 20, 20),
                    (valid[0], valid[1], 0, 10, 0, 20),
                    (tuple((t,1) for t in range(65)), valid[1], 0, 10, 20, 20)]
    checks['strict_profile_rejects_duplicate_time_bad_order_types_and_bounds'] = (
        all(rejects(TypeError, *args) for args in malformed)
        and all(rejects(ValueError, *args) for args in wrong_values))
    extreme = energy_report(((0,60000),), ((0,20000),), 0, 10000, 10000, 10000)
    checks['inclusive_unit_domain_endpoints_max_records_and_empty_channel_policy'] = (
        extreme['net_covered_energy_J_exact'] == '12000'
        and energy_report(tuple((t,1) for t in range(64)), (), 0, 10000, 1, 1)['uncovered_ms'] == 10000
        and energy_report((), (), 0, 10, 1, 1)['durations_ms']['initial_unknown'] == 10)
    checks['inputs_and_literal_fixture_output_preserved_after_all_checks'] = (
        voltage == ((0,12000),(60,24000)) and current == ((20,500),(50,1000),(110,-250),(120,9000))
        and energy_report(voltage,current,0,120,80,40) == result)
    assert all(type(v) is bool and v for v in checks.values()), checks
    return {'python': platform.python_version(), 'implementation': platform.python_implementation(),
            'dependencies': 'standard library only; Pandas and SciPy not executed',
            'model': 'Assumed common event-time clock; zero-order hold on half-open fresh intervals, not physical reconstruction or clock synchronization',
            'units': {'time':'ms','voltage':'mV','current':'mA positive into load','integer_energy':'nJ','one_mV_mA_ms_in_J':'1/1000000000'},
            'voltage_events_mV': voltage, 'current_events_mA': current,
            'max_age_ms': {'voltage':80,'current':40}, 'result': result,
            'unknown_intervals_are_not_inferred_zero_or_total_window_energy': True,
            'independent_oracle_cases_inside_ONE_family': cases,
            'checks': checks, 'unique_check_groups':len(checks)}


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

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

{
  "python": "3.14.5",
  "implementation": "CPython",
  "dependencies": "standard library only; Pandas and SciPy not executed",
  "model": "Assumed common event-time clock; zero-order hold on half-open fresh intervals, not physical reconstruction or clock synchronization",
  "units": {
    "time": "ms",
    "voltage": "mV",
    "current": "mA positive into load",
    "integer_energy": "nJ",
    "one_mV_mA_ms_in_J": "1/1000000000"
  },
  "voltage_events_mV": [
    [
      0,
      12000
    ],
    [
      60,
      24000
    ]
  ],
  "current_events_mA": [
    [
      20,
      500
    ],
    [
      50,
      1000
    ],
    [
      110,
      -250
    ],
    [
      120,
      9000
    ]
  ],
  "max_age_ms": {
    "voltage": 80,
    "current": 40
  },
  "result": {
    "window_ms": [
      0,
      120
    ],
    "intervals": [
      {
        "interval_ms": [
          0,
          20
        ],
        "status": "initial_unknown",
        "voltage_state": "fresh",
        "current_state": "unknown",
        "voltage_source_time_ms": 0,
        "current_source_time_ms": null,
        "voltage_mV": 12000,
        "current_mA": null,
        "covered_energy_nJ": null
      },
      {
        "interval_ms": [
          20,
          50
        ],
        "status": "covered",
        "voltage_state": "fresh",
        "current_state": "fresh",
        "voltage_source_time_ms": 0,
        "current_source_time_ms": 20,
        "voltage_mV": 12000,
        "current_mA": 500,
        "covered_energy_nJ": 180000000
      },
      {
        "interval_ms": [
          50,
          60
        ],
        "status": "covered",
        "voltage_state": "fresh",
        "current_state": "fresh",
        "voltage_source_time_ms": 0,
        "current_source_time_ms": 50,
        "voltage_mV": 12000,
        "current_mA": 1000,
        "covered_energy_nJ": 120000000
      },
      {
        "interval_ms": [
          60,
          90
        ],
        "status": "covered",
        "voltage_state": "fresh",
        "current_state": "fresh",
        "voltage_source_time_ms": 60,
        "current_source_time_ms": 50,
        "voltage_mV": 24000,
        "current_mA": 1000,
        "covered_energy_nJ": 720000000
      },
      {
        "interval_ms": [
          90,
          110
        ],
        "status": "stale",
        "voltage_state": "fresh",
        "current_state": "stale",
        "voltage_source_time_ms": 60,
        "current_source_time_ms": 50,
        "voltage_mV": 24000,
        "current_mA": null,
        "covered_energy_nJ": null
      },
      {
        "interval_ms": [
          110,
          120
        ],
        "status": "covered",
        "voltage_state": "fresh",
        "current_state": "fresh",
        "voltage_source_time_ms": 60,
        "current_source_time_ms": 110,
        "voltage_mV": 24000,
        "current_mA": -250,
        "covered_energy_nJ": -60000000
      }
    ],
    "durations_ms": {
      "covered": 80,
      "initial_unknown": 20,
      "stale": 20
    },
    "uncovered_ms": 40,
    "positive_covered_energy_nJ": 1020000000,
    "negative_covered_energy_nJ": -60000000,
    "net_covered_energy_nJ": 960000000,
    "net_covered_energy_J_exact": "24/25",
    "model_covers_entire_window": false
  },
  "unknown_intervals_are_not_inferred_zero_or_total_window_energy": true,
  "independent_oracle_cases_inside_ONE_family": 4489,
  "checks": {
    "literal_interval_contributions_and_signed_known_energy_24_over25_J": true,
    "end_cutoff_sample_has_zero_duration_no_future_backfill": true,
    "observed_zero_current_is_covered_zero_not_missing_zero_substitution": true,
    "expiry_splits_interval_even_without_new_event_and_equality_is_stale": true,
    "event_on_expiry_replaces_old_sample_at_left_boundary": true,
    "split_horizon_additivity_uses_past_state_at_each_new_start": true,
    "current_sign_reversal_negates_net_not_coverage_and_swaps_signed_parts": true,
    "time_origin_and_explicit_time_scale_have_different_unit_effects": true,
    "voltage_scaling_doubles_energy_with_same_coverage": true,
    "independent_rational_midpoint_fullscan_oracle_ONE_family": true,
    "main_fixture_matches_independent_cell_reference": true,
    "strict_profile_rejects_duplicate_time_bad_order_types_and_bounds": true,
    "inclusive_unit_domain_endpoints_max_records_and_empty_channel_policy": true,
    "inputs_and_literal_fixture_output_preserved_after_all_checks": true
  },
  "unique_check_groups": 14
}

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

۵. چهار پرسش دربارهٔ زمان و انرژیِ نامعلوم

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

پرسش اول: چرا دادهٔ زمانِ ۲۰ را به ابتدای پنجره نبریم؟ پاسخ: مقدارِ جریانِ قبل از ۲۰ داده نشده و فرضِ ما نگهداریِ آخرین نمونهٔ قبلی است. پرکردن از آینده، مدل را عوض می‌کند. مجهول‌بودنِ این بخش با جریانِ مشاهده‌شدهٔ صفر فرق دارد؛ دومی می‌تواند مدتِ پوششِ معتبر و انرژیِ صفر داشته باشد.

پرسش دوم: اگر بعد از ۵۰ هیچ رویدادی تا ۱۱۰ نیاید، چه چیزی در ۹۰ تغییر می‌کند؟ پاسخ: پایانِ اعتبارِ جریان رخ می‌دهد. الگوریتم باید این زمان را نیز بشکند؛ فقط پیمایشِ زمان‌های دریافت‌شده، بازهٔ کهنه را پنهان می‌کند. صفرکردنِ جریان در این فاصله نیز برآوردِ انرژی نیست؛ عددِ ناشناخته را به عددِ معلوم تبدیل کرده‌ایم.

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

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

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

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