در یک تخمین‌گر، ترتیبِ «پیش‌بینی» و «دیدنِ اندازه‌گیری» جزئی از مدل است. اگر پیش از نخستین مشاهده، افزایشِ واریانسِ فرایند را فراموش کنیم، ممکن است کد همچنان عددی معقول بدهد ولی مسئلهٔ دیگری را حل کند. این درس یک فیلتر Kalman یک‌بعدی را برای ولتاژهای کاملاً ساختگی پیاده می‌کند و با مرجعِ دسته‌ایِ دقیق می‌سنجد. هدف، کنترلِ محاسبه و مرزِ فرض‌هاست؛ نه حذفِ نویزِ اندازه‌گیری‌شده، تخمینِ حرکتِ واقعی یا بهبودِ عملکردِ یک تجهیز.

۱. مدل گام‌تصادفی و معنای واریانس

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

مدلِ انتخابی x[k] = x[k-1] + w[k] و z[k] = x[k] + v[k] است. x حالتِ پنهان و z مشاهده است؛ هر دو با واحد V بیان می‌شوند. تغییرهای w و خطاهای v را صفرمیانگین، Gaussian و مستقل از یکدیگر و از حالتِ آغاز می‌گیریم. واریانسِ تغییرِ هر گام Q و واریانسِ مشاهده R ثابت‌اند. این فرض‌ها از دادهٔ واقعی برآورد نشده‌اند؛ پارامترهایی هستند که خودمان به مسئله داده‌ایم.

میانگینِ آغاز را m0 و واریانسِ آغاز را P0 می‌نامیم. P0، Q و R واحد V² دارند؛ خودِ انحراف معیار نیستند. در قراردادِ این برنامه، P0 و Q از صفر تا شانزده و R از ۱/۲۵۶ تا شانزده پذیرفته می‌شوند. حدِ پایینِ مثبتِ R برای دامنهٔ محلیِ محاسبه انتخاب شده است؛ مشخصهٔ حسگر یا حدِ فیزیکیِ نویز نیست. حالتِ R صفر عمداً بیرون قرارداد است. P0 یا Q صفر مجازند و به حالتِ قطعی یا تغییرِ صفر در همین مدل اشاره می‌کنند.

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

۲. جدایی پیش‌بینی و تصحیح

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

در پیش‌بینی، میانگین ثابت می‌ماند و Pminus = P + Q می‌شود. سپس نوآوری برابر مشاهده منهای میانگینِ پیش‌بینی و بهره برابر K = Pminus/(Pminus+R) است. میانگینِ جدید، میانگینِ پیش‌بینی به اضافهٔ K برابر نوآوری است. در دامنهٔ ما، بهره بی‌واحد و از صفر تا کمتر از یک است. مشاهده و حالت واحد یکسان دارند؛ مدلِ سنجشِ با ضریبِ تبدیلِ دلخواه پیاده نشده است.

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

واریانسِ پسین را به شکلِ اسکالرِ Joseph حساب می‌کنیم: Pnew = (1-K)²*Pminus + K²*R. تابعِ کاملِ update در منبع FilterPy این ساختار را برای حالتِ ماتریسی نشان می‌دهد. آن را برای مدلِ تک‌متغیرهٔ خودمان تخصصی کرده‌ایم؛ اجرای FilterPy گزارش نمی‌کنیم. برابریِ جبری با رابطهٔ کوتاه‌تر، تضمینِ برابریِ تمام خطاهای ممیز شناور یا دقتِ نامحدود نیست.

۳. کد کاملِ حالت یک‌بعدی محدود

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

فایل scalar_kalman_contract.py با Python 3.14.5 و فقط کتابخانهٔ استاندارد اجرا شده است. فرمانِ بازسازی python scalar_kalman_contract.py است. FilterPy و SciPy نصب نیستند؛ NumPy، MATLAB یا چارچوبِ یادگیری نیز در اجرای این فایل استفاده نشده‌اند. هر گام، پیش‌بینی، نوآوری، بهره و حالتِ پس از تصحیح را نگه می‌دارد تا اشتباهِ ترتیب پشتِ آخرین عدد پنهان نماند.

"""Bounded scalar random-walk filter; independent exact batch conditioning."""
from dataclasses import asdict, dataclass
from fractions import Fraction
from itertools import product
import json
import math
import platform

ABS_TOL = 1e-11
REL_TOL = 1e-11


@dataclass(frozen=True)
class Step:
    predicted_mean_v: float
    predicted_variance_v2: float
    innovation_v: float
    gain: float
    mean_v: float
    variance_v2: float


@dataclass(frozen=True)
class Result:
    steps: tuple
    final_mean_v: float
    final_variance_v2: float


def number(value, lower, upper):
    if type(value) not in (int, float) or not lower <= value <= upper:
        raise ValueError('exact finite int/float outside chosen range')
    value = float(value)
    if not math.isfinite(value):
        raise ValueError('nonfinite value')
    return value


def _predict(mean, variance, process_variance):
    return mean, variance + process_variance


def _correct(predicted_mean, predicted_variance, observation, measurement_variance):
    innovation = observation - predicted_mean
    gain = predicted_variance / (predicted_variance + measurement_variance)
    mean = predicted_mean + gain * innovation
    # Scalar Joseph covariance form, with H=1.
    variance = ((1.0 - gain) ** 2 * predicted_variance
                + gain * gain * measurement_variance)
    return Step(predicted_mean, predicted_variance, innovation, gain, mean, variance)


def kalman(observations, initial_mean, initial_variance, process_variance, measurement_variance):
    """One transition before EVERY observation, including the first."""
    if type(observations) is not tuple or not 0 <= len(observations) <= 16:
        raise ValueError('observations must be an exact tuple with 0..16 entries')
    observations = tuple(number(z, -16, 16) for z in observations)
    mean = number(initial_mean, -16, 16)
    variance = number(initial_variance, 0, 16)
    q = number(process_variance, 0, 16)
    r = number(measurement_variance, 1 / 256, 16)
    rows = []
    for z in observations:
        predicted_mean, predicted_variance = _predict(mean, variance, q)
        row = _correct(predicted_mean, predicted_variance, z, r)
        assert math.isfinite(row.mean_v) and math.isfinite(row.variance_v2)
        rows.append(row)
        mean, variance = row.mean_v, row.variance_v2
    return Result(tuple(rows), mean, variance)


def rational_solve(matrix, rhs):
    """Small exact elimination; not the floating-point production solver."""
    n = len(rhs)
    augmented = [[Fraction(x) for x in row] + [Fraction(y)]
                 for row, y in zip(matrix, rhs, strict=True)]
    for col in range(n):
        pivot = next(i for i in range(col, n) if augmented[i][col])
        augmented[col], augmented[pivot] = augmented[pivot], augmented[col]
        scale = augmented[col][col]
        augmented[col] = [x / scale for x in augmented[col]]
        for row in range(n):
            if row == col:
                continue
            multiple = augmented[row][col]
            augmented[row] = [x - multiple * y for x, y in
                              zip(augmented[row], augmented[col], strict=True)]
    return [row[-1] for row in augmented]


def batch_oracle(observations, m0, p0, q, r):
    """Condition jointly on a full observation prefix; no gain recursion."""
    n = len(observations)
    m0, p0, q, r = map(Fraction, (m0, p0, q, r))
    if n == 0:
        return m0, p0
    # Cov(z_i,z_j)=P0+min(i,j)Q+delta_ij R, i,j start at one.
    covariance = [[p0 + min(i, j) * q + (r if i == j else 0)
                   for j in range(1, n + 1)] for i in range(1, n + 1)]
    cross = [p0 + i * q for i in range(1, n + 1)]
    residuals = [Fraction(z) - m0 for z in observations]
    solved_residuals = rational_solve(covariance, residuals)
    solved_cross = rational_solve(covariance, cross)
    mean = m0 + sum(c * b for c, b in zip(cross, solved_residuals, strict=True))
    variance = p0 + n * q - sum(c * b for c, b in zip(cross, solved_cross, strict=True))
    return mean, variance


def close(a, b):
    return math.isclose(a, float(b), abs_tol=ABS_TOL, rel_tol=REL_TOL)


def rejected(*args):
    try:
        kalman(*args)
    except ValueError:
        return True
    return False


def main():
    observations = (2, 0)
    hand = kalman(observations, 0, 1, 1, 2)
    checks = {}
    checks['literal_two_step_hand_means_gains_and_variances'] = (
        hand.steps == (Step(0.0, 2.0, 2.0, 0.5, 1.0, 1.0),
                       Step(1.0, 2.0, -1.0, 0.5, 0.5, 1.0)))
    checks['literal_joint_covariance_hand_oracle'] = (
        batch_oracle(observations, 0, 1, 1, 2) == (Fraction(1, 2), Fraction(1)))
    cases, max_mean_error, max_variance_error = 0, 0.0, 0.0
    for sequence in product((-2, 0, 3), repeat=3):
        for p0, q, r in product((0, 1, 2), (0, 1), (1, 3)):
            got = kalman(sequence, 1, p0, q, r)
            for n, row in enumerate(got.steps, start=1):
                exact_mean, exact_var = batch_oracle(sequence[:n], 1, p0, q, r)
                assert close(row.mean_v, exact_mean) and close(row.variance_v2, exact_var)
                max_mean_error = max(max_mean_error, abs(row.mean_v - float(exact_mean)))
                max_variance_error = max(max_variance_error, abs(row.variance_v2 - float(exact_var)))
            cases += 1
    checks['324_sequences_all_prefixes_exact_joint_batch_oracle_one_group'] = cases == 324
    checks['empty_packet_preserves_initial_state_without_prediction'] = (
        kalman((), 3, 2, 1, 2) == Result((), 3.0, 2.0))
    certain = kalman((16, -16), 3, 0, 0, 1)
    checks['zero_initial_and_process_variance_gives_zero_gain'] = (
        certain.final_mean_v == 3 and certain.final_variance_v2 == 0
        and all(s.gain == 0 for s in certain.steps))
    zero_process = kalman((2, 4), 0, 1, 0, 2)
    checks['zero_process_matches_independent_constant_state_precision_formula'] = (
        close(zero_process.final_mean_v, Fraction(3, 2))
        and close(zero_process.final_variance_v2, Fraction(1, 2)))
    checks['first_prediction_must_include_process_variance'] = (
        hand.steps[0].gain == 0.5 and not close(hand.steps[0].mean_v, Fraction(2, 3)))
    shifted = kalman((3, 1), 1, 1, 1, 2)
    checks['common_mean_and_observation_offset'] = (
        all(close(a.mean_v + 1, b.mean_v) and close(a.variance_v2, b.variance_v2)
            for a, b in zip(hand.steps, shifted.steps, strict=True)))
    scaled = kalman((-4, 0), 0, 4, 4, 8)
    checks['signal_scaling_requires_squared_covariance_scaling'] = (
        all(close(-2 * a.mean_v, b.mean_v) and close(4 * a.variance_v2, b.variance_v2)
            and close(a.gain, b.gain) for a, b in zip(hand.steps, scaled.steps, strict=True)))
    checks['gain_and_variance_bounds_in_recorded_prefixes'] = (
        all(0 <= s.gain < 1 and 0 <= s.variance_v2 <= s.predicted_variance_v2
            for s in hand.steps))
    more_uncertain = kalman((2,), 0, 1, 1, 8)
    checks['larger_supplied_measurement_variance_reduces_first_gain'] = (
        more_uncertain.steps[0].gain < hand.steps[0].gain)
    upper = kalman((16,) * 16, -16, 16, 16, 1 / 256)
    checks['inclusive_sample_value_and_variance_bounds'] = (
        len(upper.steps) == 16 and math.isfinite(upper.final_mean_v)
        and math.isfinite(upper.final_variance_v2))
    checks['missing_and_container_length_contract_rejections'] = all(rejected(z, 0, 1, 1, 2)
        for z in ([1], iter((1,)), (1,) * 17, (None,), None))
    class FloatChild(float):
        pass
    checks['numeric_type_nonfinite_and_range_contract_rejections'] = all(rejected((z,), 0, 1, 1, 2)
        for z in (True, '1', complex(1), FloatChild(1), math.nan, math.inf, 17, 10 ** 1000))
    checks['variance_and_initial_mean_contract_rejections'] = all(rejected(*args) for args in (
        ((1,), 17, 1, 1, 2), ((1,), 0, -1, 1, 2), ((1,), 0, 1, -1, 2),
        ((1,), 0, 1, 1, 0), ((1,), 0, 1, 1, 1 / 512), ((1,), 0, 1, 1, 17),
        ((1,), 0, True, 1, 2), ((1,), 0, 1, math.nan, 2)))
    assert all(type(v) is bool and v for v in checks.values())
    print(json.dumps({
        'python': platform.python_version(), 'dependencies': 'stdlib only',
        'filterpy_executed': False, 'numpy_executed': False, 'scipy_executed': False,
        'units': {'means_observations_innovations': 'V', 'variances': 'V^2', 'gain': 'dimensionless'},
        'prediction_precedes_first_observation': True,
        'hand_observations_v': observations, 'hand_result': asdict(hand),
        'hand_exact_final_mean_v': '1/2', 'hand_exact_final_variance_v2': '1',
        'batch_oracle_sequences_inside_one_group': cases,
        'prefix_comparisons_inside_same_group': cases * 3,
        'maximum_batch_oracle_absolute_mean_difference_v': max_mean_error,
        'maximum_batch_oracle_absolute_variance_difference_v2': max_variance_error,
        'absolute_tolerance_in_each_quantity_unit': ABS_TOL, 'relative_tolerance': REL_TOL,
        'checks': checks, 'unique_check_groups': len(checks), 'all_passed': True,
        'limits': 'Supplied Gaussian random-walk model only; no learned noise, missing data, actual timing, smoothing, hardware accuracy or noise-reduction efficacy claim.'
    }, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()

کل ورودی پیش از حلقه اعتبارسنجی می‌شود. bool، زیرکلاسِ float، رشته، مختلط، None، NaN، بی‌نهایت و عددِ بیرونِ دامنه خطا می‌دهند. None را مشاهدهٔ صفر یا دستورِ پیش‌بینیِ تنها نمی‌دانیم. تابعِ اصلی، ورودی یا یک حالتِ مشترکِ بیرونی را تغییر نمی‌دهد و خروجی‌های frozen می‌سازد. این ویژگی مربوط به همین رابطِ محدود است؛ تضمینِ تراکنش، مصرفِ حافظهٔ ثابتِ کل برنامه یا کنترلِ یک سامانهٔ زنده نیست.

نمونهٔ دستی با m0 صفر ولت، P0 یک ولت‌مربع، Q یک ولت‌مربع و R دو ولت‌مربع آغاز می‌شود؛ مشاهده‌ها دو و صفر ولت‌اند. پیش‌بینیِ اول واریانس را به دو می‌رساند، بهره یک‌دوم و میانگینِ پسین یک ولت می‌شود؛ واریانسِ پسین یک است. گام دوم دوباره واریانسِ پیش‌بینیِ دو و بهرهٔ یک‌دوم دارد؛ نوآوری منفی یک ولت است و میانگینِ نهایی نیم ولت می‌شود. واریانسِ نهایی یک ولت‌مربع باقی می‌ماند.

۴. مرجع دسته‌ایِ کسری و پاسخ دستی

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

مرجع مستقل، رابطهٔ بهره یا تابع‌های پیش‌بینی و تصحیح را تکرار نمی‌کند. برای هر پیشوندِ مشاهده، ماتریسِ کوواریانسِ مشترک را یک‌جا می‌سازد. با اندیس‌های شروع‌شونده از یک، خانهٔ i و j برابر P0 + min(i,j)*Q + delta(i,j)*R است. کوواریانسِ حالتِ آخر با مشاهدهٔ i نیز P0 + i*Q می‌شود. این‌ها از جمعِ تغییرهای مستقلِ گام‌تصادفی به دست می‌آیند؛ مشاهده‌های مختلف به علتِ حالتِ مشترک، مستقل نیستند.

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

{
  "python": "3.14.5",
  "dependencies": "stdlib only",
  "filterpy_executed": false,
  "numpy_executed": false,
  "scipy_executed": false,
  "units": {
    "means_observations_innovations": "V",
    "variances": "V^2",
    "gain": "dimensionless"
  },
  "prediction_precedes_first_observation": true,
  "hand_observations_v": [
    2,
    0
  ],
  "hand_result": {
    "steps": [
      {
        "predicted_mean_v": 0.0,
        "predicted_variance_v2": 2.0,
        "innovation_v": 2.0,
        "gain": 0.5,
        "mean_v": 1.0,
        "variance_v2": 1.0
      },
      {
        "predicted_mean_v": 1.0,
        "predicted_variance_v2": 2.0,
        "innovation_v": -1.0,
        "gain": 0.5,
        "mean_v": 0.5,
        "variance_v2": 1.0
      }
    ],
    "final_mean_v": 0.5,
    "final_variance_v2": 1.0
  },
  "hand_exact_final_mean_v": "1/2",
  "hand_exact_final_variance_v2": "1",
  "batch_oracle_sequences_inside_one_group": 324,
  "prefix_comparisons_inside_same_group": 972,
  "maximum_batch_oracle_absolute_mean_difference_v": 4.440892098500626e-16,
  "maximum_batch_oracle_absolute_variance_difference_v2": 4.440892098500626e-16,
  "absolute_tolerance_in_each_quantity_unit": 1e-11,
  "relative_tolerance": 1e-11,
  "checks": {
    "literal_two_step_hand_means_gains_and_variances": true,
    "literal_joint_covariance_hand_oracle": true,
    "324_sequences_all_prefixes_exact_joint_batch_oracle_one_group": true,
    "empty_packet_preserves_initial_state_without_prediction": true,
    "zero_initial_and_process_variance_gives_zero_gain": true,
    "zero_process_matches_independent_constant_state_precision_formula": true,
    "first_prediction_must_include_process_variance": true,
    "common_mean_and_observation_offset": true,
    "signal_scaling_requires_squared_covariance_scaling": true,
    "gain_and_variance_bounds_in_recorded_prefixes": true,
    "larger_supplied_measurement_variance_reduces_first_gain": true,
    "inclusive_sample_value_and_variance_bounds": true,
    "missing_and_container_length_contract_rejections": true,
    "numeric_type_nonfinite_and_range_contract_rejections": true,
    "variance_and_initial_mean_contract_rejections": true
  },
  "unique_check_groups": 15,
  "all_passed": true,
  "limits": "Supplied Gaussian random-walk model only; no learned noise, missing data, actual timing, smoothing, hardware accuracy or noise-reduction efficacy claim."
}

در یک گروه، ۳۲۴ دنباله و ترکیبِ پارامتر، با هر سه پیشوندشان مقایسه شدند؛ ۹۷۲ مقایسهٔ داخلی، گروه‌های تازه نیستند. بیشترین اختلافِ ثبت‌شدهٔ میانگین حدود ۴٫۴۴ ضرب‌در ده به توان منفی شانزده ولت و اختلافِ واریانس همین اندازه در واحد V² بود. تلورانسِ مطلقِ ده به توان منفی یازده در واحدِ هر کمیت و تلورانسِ نسبیِ هم‌اندازه برای این آزمایش انتخاب شده‌اند. جمعاً پانزده گروه گذشت؛ دامنهٔ محدود، جای اثباتِ همهٔ ورودی‌های ممکن را نمی‌گیرد.

۵. چهار پرسش و مرز ادعای تخمین

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

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

پرسش دوم: چرا برای تغییرِ مقیاس، فقط ولتاژها را ضرب نکنیم؟ پاسخ: اگر همهٔ مقدارهای ولتاژ در منفی دو ضرب شوند، P0، Q و R باید در چهار ضرب شوند. بهره ثابت، میانگین‌ها منفی دو برابر و واریانس‌ها چهار برابر می‌شوند. تغییرِ فقط یکی از این بخش‌ها، مسئلهٔ آماری را عوض می‌کند.

پرسش سوم: Q صفر همان میانگینِ ساده است؟ پاسخ: الزاماً نه؛ پیشین نیز وزن دارد. با میانگینِ آغاز صفر، P0 یک، R دو و مشاهده‌های دو و چهار، میانگینِ نهایی سه‌دوم و واریانس یک‌دوم است، نه میانگینِ سه‌ولتیِ دو مشاهده. مرجعِ مستقلِ جمعِ دقت‌ها این حالت را جدا بررسی می‌کند.

پرسش چهارم: گذشتنِ آزمون‌ها یعنی نویز کاهش یافته است؟ پاسخ: خیر؛ حالتِ حقیقی یا خطای واقعی برای مقایسه نداریم. ضریب فیلترِ نمایی و خطای عددی مسئلهٔ دیگری با بهرهٔ مبتنی بر ثابتِ زمانی است؛ اینجا بهره از واریانسِ مدل می‌آید. واریانسِ جریانیِ مشاهده‌ها نیز خودکار Q یا R را تعیین نمی‌کند. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ مستندات FilterPy نسخهٔ 1.4.4 و تاریخِ متادیتای اثبات، ۲۰ مارس ۲۰۲۰، جدا از تاریخِ دسترسی ثبت شده‌اند. این آموزش، ارزیابیِ تجهیزات یا خبرِ پژوهشیِ تازه نیست.

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

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