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

۱. قرارداد سیگنال و پنجره‌های هم‌پوشان

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

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

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

مرجع sliding_window_view در NumPy 2.4، بُعد پنجره را در انتهای شکل خروجی قرار می‌دهد. در حالت یک‌بعدیِ ما، تعداد ردیف‌ها برابر N−W+1 است. خود API حالت‌های چندبعدی و محورهای دیگر هم دارد؛ تابعِ آموزشیِ این درس فقط یک سیگنال و یک عرض مثبت را می‌پذیرد و جایگزینِ همهٔ حالت‌های آن نیست.

ورودی باید ndarray واقعی با نوع دقیق float64، یک بُعد و طول یک تا شانزده باشد. همهٔ مقدارها متناهی و قدر مطلقشان حداکثر هزار است. عرض دقیقاً int پایتون، از یک تا طول منبع است؛ bool، شناور و رشته پذیرفته نیستند. این کران‌ها برای بازبینیِ دستی‌اند، نه محدودهٔ کاریِ حسگر یا مدل. تابع تبدیل نوع، کم‌کردن میانگین یا استانداردسازی انجام نمی‌دهد.

۲. نمای فقط‌خواندنی، مالکیت و تغییر منبع

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

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

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

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

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

۳. کد کامل و نسخهٔ مستقل داده

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

فایل sliding_window_aliasing.py با Python 3.14.5 و NumPy 2.4.6 اجرا شده است. make_windows ابتدا نوع، شکل و دامنه را کنترل می‌کند و سپس نما و کپی را برمی‌گرداند. ساختن این دو خروجی، خودِ منبع را تغییر نمی‌دهد. در ادامهٔ آزمایش، تغییر منبع عمدی است و با همان بررسیِ «سازنده ورودی را دست نمی‌زند» اشتباه گرفته نمی‌شود.

"""Small floating sample windows: a read-only view is not a frozen source."""
import json
import math
import platform

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view


def make_windows(source, width):
    if type(source) is not np.ndarray or source.dtype != np.dtype("float64"):
        raise TypeError("an actual float64 ndarray is required")
    if source.ndim != 1 or not 1 <= source.size <= 16:
        raise ValueError("one-dimensional length 1..16 required")
    if not np.isfinite(source).all() or np.any(np.abs(source) > 1000):
        raise ValueError("finite values with magnitude at most 1000 required")
    if type(width) is not int or not 1 <= width <= source.size:
        raise ValueError("actual integer window width within the source required")
    view = sliding_window_view(source, width, writeable=False)
    snapshot = view.copy(order="C")
    return view, snapshot


def independent_windows(values, width):
    """A trusted tuple fixture; scalar indexing only, no NumPy/window helper."""
    return tuple(tuple(values[start + offset] for offset in range(width))
                 for start in range(len(values) - width + 1))


def tuples(array):
    return tuple(tuple(row) for row in array.tolist())


def shared(a, b):
    return bool(np.shares_memory(a, b, max_work=1000))


def rejects(exception, function, *args):
    try:
        function(*args)
    except exception:
        return True
    return False


def attempt_write(array):
    array[0, 0] = 9.0


def main():
    literal = (-0.75, 0.125, 0.5, -0.25, 0.875, -0.5, 0.25)
    source = np.array(literal, dtype=np.float64)
    view, snapshot = make_windows(source, 4)
    before = tuples(snapshot)
    hand = ((-0.75, 0.125, 0.5, -0.25), (0.125, 0.5, -0.25, 0.875),
            (0.5, -0.25, 0.875, -0.5), (-0.25, 0.875, -0.5, 0.25))
    checks = {"literal_windows_and_shape": tuples(view) == hand and view.shape == (4, 4)}
    checks["view_shares_source_snapshot_does_not"] = shared(view, source) and not shared(snapshot, source)
    checks["default_view_is_read_only_and_assignment_rejected"] = (
        not view.flags.writeable and rejects(ValueError, attempt_write, view))
    source[3] = -1.0
    changed_positions = tuple((r, c) for r in range(4) for c in range(4)
                              if view[r, c] != snapshot[r, c])
    checks["source_edit_changes_all_four_alias_positions"] = (
        changed_positions == ((0, 3), (1, 2), (2, 1), (3, 0))
        and all(view[r, c] == -1.0 for r, c in changed_positions))
    checks["snapshot_preserves_original_values_after_source_edit"] = tuples(snapshot) == hand
    snapshot[0, 0] = 0.0
    checks["detached_snapshot_is_writeable_not_immutable"] = (
        snapshot.flags.writeable and snapshot.flags.owndata
        and source[0] == -0.75 and view[0, 0] == -0.75)
    checks["logical_nbytes_count_positions_not_distinct_storage"] = (
        source.itemsize == 8 and source.nbytes == 56 and view.nbytes == 128
        and snapshot.nbytes == 128 and view.size == 16 and not view.flags.owndata)

    cases, oracle_ok = 0, True
    for length in range(1, 9):
        values = tuple(((i * 3) % 11 - 5) / 8 for i in range(length))
        for reversed_order in (False, True):
            raw = np.array(values, dtype=np.float64)
            sample = raw[::-1] if reversed_order else raw
            expected_values = values[::-1] if reversed_order else values
            for width in range(1, length + 1):
                windows, copy = make_windows(sample, width)
                expected = independent_windows(expected_values, width)
                oracle_ok = oracle_ok and tuples(windows) == expected and tuples(copy) == expected
                oracle_ok = oracle_ok and shared(windows, sample) and not shared(copy, sample)
                cases += 1
    checks["independent_positional_oracle_72_cases_one_group"] = oracle_ok and cases == 72
    only, _ = make_windows(np.array([0.25], dtype=np.float64), 1)
    full, _ = make_windows(np.array(literal, dtype=np.float64), len(literal))
    checks["one_sample_and_full_width_have_no_padding"] = only.shape == (1, 1) and full.shape == (1, 7)
    checks["width_type_and_bounds_rejected"] = all(
        rejects(ValueError, make_windows, source, width) for width in (True, 1.0, 0, -1, 8, "4"))
    checks["container_dtype_shape_and_values_rejected"] = all(
        rejects((TypeError, ValueError), make_windows, bad, 1) for bad in (
            list(literal), np.array([1], dtype=np.int64), np.array([True]),
            np.array([], dtype=np.float64), np.zeros((1, 2)), np.zeros(17),
            np.array([math.nan]), np.array([math.inf]), np.array([1000.1])))
    clean = np.array(literal, dtype=np.float64)
    expected_clean = clean.copy()
    make_windows(clean, 3)
    checks["constructing_windows_does_not_mutate_source"] = np.array_equal(clean, expected_clean)
    clean.flags.writeable = False
    ro_view, detached = make_windows(clean, 3)
    detached[0, 0] = 1.0
    checks["read_only_source_still_allows_independent_writeable_copy"] = (
        not ro_view.flags.writeable and detached.flags.writeable and clean[0] == literal[0])
    checks = {key: bool(value) for key, value in checks.items()}
    if not all(checks.values()):
        raise RuntimeError(checks)
    return {"python_version": platform.python_version(), "numpy_version": np.__version__,
            "data_origin": "original synthetic dimensionless samples; no signal acquisition",
            "source_before": literal, "window_width": 4, "windows_before": before,
            "source_after_index_three_edit": source.tolist(), "view_after_source_edit": view.tolist(),
            "changed_window_positions": changed_positions,
            "logical_element_bytes": {"source": source.nbytes, "view": view.nbytes,
                                      "independent_copy": snapshot.nbytes},
            "rss_allocation_or_speed_measured": False,
            "independent_oracle_cases_inside_one_group": cases,
            "checks": checks, "unique_check_groups": len(checks)}


if __name__ == "__main__":
    print(json.dumps(main(), ensure_ascii=False, indent=2, allow_nan=False))

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

دنبالهٔ معکوس‌شده هم در محدودهٔ این تابع پذیرفته می‌شود؛ ترتیبِ منطقیِ نمونه‌های همان آرایه ملاک است. آزمون روی نمایی با stride منفی اجرا شده، ولی ساخت دستیِ stride دلخواه انجام نمی‌دهیم. این کار با API کم‌سطح as_strided تفاوت دارد. پذیرش این نمونهٔ منظم، مجوز ساختِ نمای ناایمن یا تضمین همهٔ آرایه‌های خارجی نیست.

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

۴. مرجع جایگاهی و شمار خانه‌های منطقی

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

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

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

{
  "python_version": "3.14.5",
  "numpy_version": "2.4.6",
  "data_origin": "original synthetic dimensionless samples; no signal acquisition",
  "source_before": [
    -0.75,
    0.125,
    0.5,
    -0.25,
    0.875,
    -0.5,
    0.25
  ],
  "window_width": 4,
  "windows_before": [
    [
      -0.75,
      0.125,
      0.5,
      -0.25
    ],
    [
      0.125,
      0.5,
      -0.25,
      0.875
    ],
    [
      0.5,
      -0.25,
      0.875,
      -0.5
    ],
    [
      -0.25,
      0.875,
      -0.5,
      0.25
    ]
  ],
  "source_after_index_three_edit": [
    -0.75,
    0.125,
    0.5,
    -1.0,
    0.875,
    -0.5,
    0.25
  ],
  "view_after_source_edit": [
    [
      -0.75,
      0.125,
      0.5,
      -1.0
    ],
    [
      0.125,
      0.5,
      -1.0,
      0.875
    ],
    [
      0.5,
      -1.0,
      0.875,
      -0.5
    ],
    [
      -1.0,
      0.875,
      -0.5,
      0.25
    ]
  ],
  "changed_window_positions": [
    [
      0,
      3
    ],
    [
      1,
      2
    ],
    [
      2,
      1
    ],
    [
      3,
      0
    ]
  ],
  "logical_element_bytes": {
    "source": 56,
    "view": 128,
    "independent_copy": 128
  },
  "rss_allocation_or_speed_measured": false,
  "independent_oracle_cases_inside_one_group": 72,
  "checks": {
    "literal_windows_and_shape": true,
    "view_shares_source_snapshot_does_not": true,
    "default_view_is_read_only_and_assignment_rejected": true,
    "source_edit_changes_all_four_alias_positions": true,
    "snapshot_preserves_original_values_after_source_edit": true,
    "detached_snapshot_is_writeable_not_immutable": true,
    "logical_nbytes_count_positions_not_distinct_storage": true,
    "independent_positional_oracle_72_cases_one_group": true,
    "one_sample_and_full_width_have_no_padding": true,
    "width_type_and_bounds_rejected": true,
    "container_dtype_shape_and_values_rejected": true,
    "constructing_windows_does_not_mutate_source": true,
    "read_only_source_still_allows_independent_writeable_copy": true
  },
  "unique_check_groups": 13
}

در محیط ثبت‌شده هر float64 هشت بایت دارد. هفت عضوِ منبع، nbytes برابر ۵۶ و شانزده جایگاهِ نما، nbytes برابر ۱۲۸ می‌دهند؛ کپی نیز ۱۲۸ دارد. طبق مرجع nbytes، این ویژگی بایتِ عنصرها را می‌شمارد و سربارِ فراداده را شامل نمی‌شود. در نمای هم‌پوشان، جایگاه‌های منطقی تکراری‌اند؛ عدد ۱۲۸ شاهد تخصیصِ یک بافر مستقلِ ۱۲۸بایتی برای نما نیست.

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

۵. چهار پرسش با پاسخ کامل

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

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

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

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

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

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

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