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

۱. باقی‌ماندهٔ حسگر و واحد میلی‌ولت را روشن کنیم

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

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

تابع tuple با صفر تا ۶۴ عضو می‌گیرد. هر عضو باید int دقیقِ منفی هزار تا مثبت هزار میلی‌ولت باشد؛ bool، شناور، رشته، NaN و بی‌نهایت پذیرفته نیستند. فقط برچسبِ دقیق mV مجاز است و تغییر آن به V، تبدیل واحد انجام نمی‌دهد. دامنه و گامِ صحیحِ میلی‌ولت، انتخابِ نرم‌افزاریِ همین آموزش‌اند و تفکیک یا دقتِ یک مبدل واقعی معرفی نمی‌شوند.

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

۲. حالت تجمعی را با مرجع و آستانهٔ هم‌واحد بسازیم

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

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

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

راهنمای CUSUM در Dataplot، رابطه را بر اساس z-scoreِ میانگینِ زیرگروه توضیح می‌دهد. آن نمایه استانداردشده است و با باقی‌ماندهٔ میلی‌ولتیِ حاضر فرق دارد. Dataplot اجرا نشده و تنظیمِ پیش‌فرضش را انتخابِ ما نمی‌نامیم. k و h این تمرین فرض‌های تألیفی‌اند؛ از احتمال هشدار کاذب، نرخ کشف یا پیشنهادِ تنظیم حسگر استخراج نشده‌اند.

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

۳. برابری آستانه، افت حالت و ادامهٔ قطعه را جدا کنیم

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

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

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

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

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

۴. کد را با بیشینهٔ جمعِ پسوندها بسنجیم

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

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

"""Own upper tabular CUSUM on supplied integer-mV residuals, no reset or latch."""
from itertools import product
import json
import platform

STATE_LIMIT_MV = 1_000_000


def upper_cusum(residuals, k, h, initial=0, unit="mV"):
    if type(unit) is not str or unit != "mV":
        raise ValueError("unit must be exactly mV; no conversion is performed")
    if type(residuals) is not tuple or len(residuals) > 64:
        raise ValueError("residuals must be a tuple with 0..64 entries")
    if any(type(r) is not int or not -1000 <= r <= 1000 for r in residuals):
        raise ValueError("each residual must be actual int in -1000..1000 mV")
    if type(k) is not int or not 0 <= k <= 1000:
        raise ValueError("k must be actual int in 0..1000 mV")
    if type(h) is not int or not 1 <= h <= 100_000:
        raise ValueError("h must be actual int in 1..100000 mV")
    if type(initial) is not int or not 0 <= initial <= STATE_LIMIT_MV:
        raise ValueError("initial state outside the declared nonnegative mV domain")
    state = initial
    states, flags = [], []
    for residual in residuals:
        state = max(0, state + residual - k)
        if state > STATE_LIMIT_MV:
            raise ValueError("computed state exceeds the declared mV domain")
        states.append(state)
        flags.append(state > h)
    return {"states_mV": tuple(states), "above_h": tuple(flags), "final_mV": state}


def suffix_oracle(residuals, k, initial):
    """Independent full-prefix/suffix sums; no CUSUM recurrence or main helper."""
    states = []
    for end in range(1, len(residuals) + 1):
        terms = tuple(value - k for value in residuals[:end])
        candidates = [0, initial + sum(terms)]
        candidates.extend(sum(terms[start:]) for start in range(end))
        states.append(max(candidates))
    return tuple(states)


def rejects(call):
    try:
        call()
    except ValueError:
        return True
    return False


def main():
    residuals = (0, 2, 2, 2, 2, 2, -3, 2)
    expected_states = (0, 1, 2, 3, 4, 5, 1, 2)
    expected_flags = (False, False, False, False, False, True, False, False)
    result = upper_cusum(residuals, 1, 4)
    checks = {}
    checks["literal_hand_states_and_strict_threshold_flags"] = (
        result["states_mV"] == expected_states and result["above_h"] == expected_flags)
    checks["equality_four_is_not_flagged_and_five_is_flagged"] = (
        result["states_mV"][4:6] == (4, 5) and result["above_h"][4:6] == (False, True))
    checks["flag_does_not_reset_state_or_latch_future_flags"] = (
        result["states_mV"][5:] == (5, 1, 2) and result["above_h"][5:] == (True, False, False))
    checks["negative_excursion_floors_at_zero_not_negative_state"] = (
        upper_cusum((-3, 2), 1, 4)["states_mV"] == (0, 1))
    checks["residual_equal_to_k_has_zero_increment"] = (
        upper_cusum((1, 1, 1), 1, 4, initial=3)["states_mV"] == (3, 3, 3))
    continuation_ok = True
    for cut in range(len(residuals) + 1):
        first = upper_cusum(residuals[:cut], 1, 4)
        second = upper_cusum(residuals[cut:], 1, 4, initial=first["final_mV"])
        continuation_ok = continuation_ok and (
            first["states_mV"] + second["states_mV"] == result["states_mV"] and
            first["above_h"] + second["above_h"] == result["above_h"] and
            second["final_mV"] == result["final_mV"])
    checks["all_nine_chunk_cuts_preserve_state_and_flags_one_group"] = continuation_ok
    wrong = upper_cusum(residuals[4:], 1, 4)
    checks["unrequested_chunk_reset_changes_history_and_misses_flag"] = (
        wrong["states_mV"] == (1, 2, 0, 1) and wrong["above_h"] == (False,) * 4 and
        wrong["states_mV"] != result["states_mV"][4:])
    checks["reordering_residuals_changes_cumulative_history"] = (
        upper_cusum((2, -3), 1, 4)["final_mV"] == 0 and
        upper_cusum((-3, 2), 1, 4)["final_mV"] == 1)
    cases, oracle_ok = 0, True
    for length in range(5):
        for sequence in product((-2, 0, 2), repeat=length):
            for allowance in (0, 1, 2):
                for initial in (0, 3):
                    actual = upper_cusum(sequence, allowance, 4, initial)
                    expected = suffix_oracle(sequence, allowance, initial)
                    oracle_ok = oracle_ok and actual["states_mV"] == expected
                    oracle_ok = oracle_ok and actual["above_h"] == tuple(v > 4 for v in expected)
                    oracle_ok = oracle_ok and actual["final_mV"] == (expected[-1] if expected else initial)
                    cases += 1
    checks["all_726_finite_paths_match_independent_suffix_sums_one_group"] = oracle_ok and cases == 726
    checks["empty_chunk_preserves_initial_state_without_new_flag"] = (
        upper_cusum((), 1, 4, 5) == {"states_mV": (), "above_h": (), "final_mV": 5})
    checks["changing_only_threshold_preserves_states"] = (
        upper_cusum(residuals, 1, 5)["states_mV"] == expected_states and
        upper_cusum(residuals, 1, 5)["above_h"] == (False,) * 8)
    checks["closed_bounds_and_state_domain_error_are_explicit"] = (
        upper_cusum((1000,) * 64, 0, 100_000)["final_mV"] == 64_000 and
        upper_cusum((-1000,), 1000, 1)["final_mV"] == 0 and
        upper_cusum((), 0, 1, STATE_LIMIT_MV)["final_mV"] == STATE_LIMIT_MV and
        rejects(lambda: upper_cusum((1,), 0, 1, STATE_LIMIT_MV)))
    checks["residual_container_type_length_and_values_rejected"] = all(
        rejects(lambda value=value: upper_cusum(value, 1, 4))
        for value in ([], None, (0,) * 65, (True,), (False,), (2.0,), (1001,), (-1001,),
                      (float("nan"),), (float("inf"),), (10**400,), ("2",)))
    checks["parameter_types_ranges_and_units_rejected"] = (
        all(rejects(lambda v=v: upper_cusum((0,), v, 4))
            for v in (True, 1.0, -1, 1001, 10**400)) and
        all(rejects(lambda v=v: upper_cusum((0,), 1, v))
            for v in (True, 4.0, 0, 100001, 10**400)) and
        all(rejects(lambda v=v: upper_cusum((0,), 1, 4, v))
            for v in (True, 0.0, -1, STATE_LIMIT_MV + 1, 10**400)) and
        all(rejects(lambda v=v: upper_cusum((0,), 1, 4, unit=v))
            for v in ("V", "mv", None, 1)))
    checks["input_unchanged_and_late_invalid_record_has_no_external_state"] = (
        residuals == (0, 2, 2, 2, 2, 2, -3, 2) and
        rejects(lambda: upper_cusum((2, True), 1, 4)) and
        upper_cusum(residuals, 1, 4) == result)
    assert all(type(value) is bool and value for value in checks.values())
    print(json.dumps({
        "python_version": platform.python_version(), "dependencies": "standard library only",
        "data_origin": "original supplied synthetic residuals; no baseline fit or actual sensor",
        "unit": "mV", "residuals_mV": residuals, "k_mV": 1, "h_mV": 4, "initial_mV": 0,
        "states_mV": result["states_mV"], "above_h": result["above_h"],
        "true_flag_zero_based_indices": [i for i, value in enumerate(result["above_h"]) if value],
        "final_mV": result["final_mV"],
        "independent_oracle_cases_in_one_group": cases,
        "chunk_cuts_in_one_group": len(residuals) + 1,
        "wrong_zero_restart_at_cut4": wrong,
        "declared_state_limit_mV": STATE_LIMIT_MV,
        "checks": checks, "unique_check_groups": len(checks),
        "limitations": ["Only upper one-sided tabular statistic; strict greater-than flag at each observation.",
                        "No automatic alarm reset, latch, baseline estimate, sigma, probability or run-length result.",
                        "No sample-time input, hardware command, real sensor, model, service health or safety outcome."]
    },ensure_ascii=False,indent=2))


if __name__ == "__main__":
    main()

مرجع مستقل، رابطهٔ بازگشتی را تکرار نمی‌کند. برای هر پیشوند، همهٔ پسوندهای «باقی‌مانده منهای k» را جدا جمع می‌کند و بیشینهٔ آن‌ها و صفر را می‌گیرد. برای حالتِ آغازِ غیرصفر، نامزدِ «حالتِ آغاز به‌علاوهٔ مجموعِ کلِ پیشوند» هم افزوده می‌شود. این مسیر به جوابِ مورد انتظار می‌رسد، بدون آنکه حالتِ قبلیِ مسیرِ اصلی را به‌روزرسانی کند.

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

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

۵. پرچم عددی را تشخیص خرابی یا فرمان ایمنی ننامیم

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

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

{
  "python_version": "3.14.5",
  "dependencies": "standard library only",
  "data_origin": "original supplied synthetic residuals; no baseline fit or actual sensor",
  "unit": "mV",
  "residuals_mV": [
    0,
    2,
    2,
    2,
    2,
    2,
    -3,
    2
  ],
  "k_mV": 1,
  "h_mV": 4,
  "initial_mV": 0,
  "states_mV": [
    0,
    1,
    2,
    3,
    4,
    5,
    1,
    2
  ],
  "above_h": [
    false,
    false,
    false,
    false,
    false,
    true,
    false,
    false
  ],
  "true_flag_zero_based_indices": [
    5
  ],
  "final_mV": 2,
  "independent_oracle_cases_in_one_group": 726,
  "chunk_cuts_in_one_group": 9,
  "wrong_zero_restart_at_cut4": {
    "states_mV": [
      1,
      2,
      0,
      1
    ],
    "above_h": [
      false,
      false,
      false,
      false
    ],
    "final_mV": 1
  },
  "declared_state_limit_mV": 1000000,
  "checks": {
    "literal_hand_states_and_strict_threshold_flags": true,
    "equality_four_is_not_flagged_and_five_is_flagged": true,
    "flag_does_not_reset_state_or_latch_future_flags": true,
    "negative_excursion_floors_at_zero_not_negative_state": true,
    "residual_equal_to_k_has_zero_increment": true,
    "all_nine_chunk_cuts_preserve_state_and_flags_one_group": true,
    "unrequested_chunk_reset_changes_history_and_misses_flag": true,
    "reordering_residuals_changes_cumulative_history": true,
    "all_726_finite_paths_match_independent_suffix_sums_one_group": true,
    "empty_chunk_preserves_initial_state_without_new_flag": true,
    "changing_only_threshold_preserves_states": true,
    "closed_bounds_and_state_domain_error_are_explicit": true,
    "residual_container_type_length_and_values_rejected": true,
    "parameter_types_ranges_and_units_rejected": true,
    "input_unchanged_and_late_invalid_record_has_no_external_state": true
  },
  "unique_check_groups": 15,
  "limitations": [
    "Only upper one-sided tabular statistic; strict greater-than flag at each observation.",
    "No automatic alarm reset, latch, baseline estimate, sigma, probability or run-length result.",
    "No sample-time input, hardware command, real sensor, model, service health or safety outcome."
  ]
}

ارزیابی زمانی پیش‌بینی حسگر دربارهٔ زمانِ دسترسی و مرزِ آموزش است؛ اینجا پیش‌بینی نداریم. مدارشکنِ نرم‌افزاری هم اجازهٔ درخواست را از روی نتیجه‌های اعلام‌شده کنترل می‌کند؛ CUSUM حاضر نه سرویس را می‌بندد و نه فرمانی به عملگر می‌دهد.

منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند. تاریخِ انتشارِ اولیهٔ صفحهٔ راهنمای NIST مشخص نیست. صفحهٔ Dataplot، عبارت‌های تاریخیِ 1998/9، 06/05/2001 و 12/04/2023 را به‌ترتیب برای پیاده‌سازی، ایجاد و آخرین ویرایش نشان می‌دهد؛ ترتیبِ ماه و روز را حدس نمی‌زنیم. این آموزش پایه، خبر تازه یا نتیجهٔ پایشِ واقعی نیست.

پرسش اول: آیا حالتِ چهار با h برابر چهار پرچم می‌گیرد؟ پاسخ: خیر؛ شرط اکیداً بزرگ‌تر است. حالتِ پنج در مشاهدهٔ ششم پرچم می‌گیرد و تنها اندیسِ پنجِ نمونه درست است.

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

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

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

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

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