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

مدل تألیفی را A*x=b میگیریم. دو مؤلفهٔ x مقدارهایی با واحد ولتاند؛ A بهرههای بیبعد و b دو مشاهده با واحد ولت دارد. سطر نخست A برابر یک و یک، و سطر دوم برابر یک و یکبهعلاوهٔ ε است. ε را دقیقاً دو به توان منفی بیست انتخاب میکنیم؛ این کسر در نمایش دودویی قابل نمایش دقیق است.
برای جواب مرجع یک و یک ولت، مشاهدهها دو و دوبهعلاوهٔ ε ولت میشوند. تفریق معادلهٔ نخست از دوم، رابطهٔ ε ضربدر مؤلفهٔ دوم برابر ε را میدهد؛ پس مؤلفهٔ دوم یک و اولی نیز یک است. دو سطر مستقلاند، اما اختلاف بسیار کم آنها اطلاعات تعیینکننده را حمل میکند. تعداد معادلهٔ کافی بهتنهایی نشان نمیدهد این اطلاعات در برابر تغییر ورودی مقاوم است.
توضیح نیک هایم دربارهٔ عدد شرط، حساسیت مسئله به اغتشاش کوچک داده را از خودِ الگوریتم جدا میکند. بهرهها در اینجا ثابت و معلوم فرض شدهاند؛ رابطهٔ خطی، همزمانی دو مشاهده و نبود خطای مدل نیز فرضاند. این نمونه یک مسئلهٔ آموزشیِ اختلاط دو مقدار است و معماری یا عملکرد محصولی را توصیف نمیکند.
در آزمون برازش خطی جریان و ولتاژ، باقیماندهٔ برازش و رتبه بررسی میشد. اینجا دستگاه مربعِ دارای جواب یکتا داریم و تمرکز روی حساسیت است. باقیماندهٔ کوچک برای یک نامزد، با نزدیک بودن همان نامزد به جواب مرجع برابر نیست؛ نام و واحد هر دو معیار باید در گزارش بیاید.
۲. قرارداد داده، نرم و معیار حساسیت

تابع فقط ماتریس دو در دو و بردار دقیقاً دو عضوی را در list یا tuple میپذیرد. هر مقدار باید int یا float واقعی پایتون، متناهی و با قدر مطلق حداکثر یکمیلیون باشد. bool، رشتهٔ عددی، مختلط، مقدار اسکالر NumPy و آرایهٔ NumPy در این ورودی محدود پذیرفته نیستند. داده در داخل به float64 تبدیل میشود؛ این انتخاب دامنهٔ همین تابع است.
حل با قرارداد نسخهٔ ۲٫۴ تابع solve انجام میشود. دستگاه تکینِ آزمون خطا میدهد و خودکار به کمترین مربعات تبدیل نمیشود. اگر محاسبه نتیجه یا برآورد شرط نامتناهی بسازد، برنامه نیز آن را موفق اعلام نمیکند. از این کنترلها ادعای تشخیص قطعیِ همهٔ ماتریسهای نزدیک به تکینی یا تضمین دقت در سراسر دامنه نمیسازیم.
همهٔ مقایسههای این مثال با نرم بینهایتاند: برای بردار، بیشینهٔ قدر مطلق مؤلفهها؛ برای ماتریس، بیشینهٔ مجموع قدر مطلق در سطر. عدد شرط برابر حاصلضرب نرم A و نرم وارون آن است. مستندات cond تفاوت گزینهها را روشن میکند؛ کد صریحاً p=np.inf دارد. حالت پیشفرض، نرم دو و محاسبه بر پایهٔ SVD است و عدد آن را با نتیجهٔ حاضر یکی نمیگیریم.
باقیمانده را b منهای حاصلضرب A در جواب میگیریم. نرم مطلق آن واحد ولت دارد و تقسیم بر نرم b، معیار نسبی بیبعد میسازد. برای b صفر، این نسبت تعریف نشده است؛ گزارش None میدهد و باقیماندهٔ مطلق را حفظ میکند. عدد شرط نیز بیبعد است، اما جای خطای ابزار، تلورانس فیزیکی یا عدم قطعیت مدل را نمیگیرد.
۳. کد کامل و مرجع کسریِ مستقل

فایل linear_solve_conditioning.py با Python 3.14.5 و NumPy 2.4.6 اجرا شده است. مسیر اصلی solve و محاسبهٔ باقیمانده را به کار میبرد. مرجع کوچک، فرمول کرامرِ دو در دو را با Fraction حساب میکند و solve، تجزیهٔ ماتریس یا مسیر باقیمانده را دوباره فراخوانی نمیکند. این مرجع فقط برای fixtureهای کسریِ مشخصشده است، نه پیشنهاد کرامر برای حل دستگاههای بزرگ.
مرجع Fraction میگوید ساختن کسر از float همان مقدار دودویی را حفظ میکند؛ لزوماً کسر اعشاریِ مورد تصور خواننده نیست. انتخاب ε دودویی، این ابهام را در شاهد حاضر کم میکند. هیچ limit_denominator یا گرد کردنِ مرجع به عدد مطلوب انجام نمیدهیم. در آزمون عدد شرط، رابطهٔ دقیقِ سطرهای وارون با مقدار شناور مقایسه میشود.
"""Bounded 2x2 linear-system review; synthetic voltage mixing, not measurements."""
import copy
import json
import math
import platform
from fractions import Fraction
import numpy as np
def pair(values):
if type(values) not in (list, tuple) or len(values) != 2:
raise ValueError("exactly two values in a list or tuple required")
result = []
for value in values:
if type(value) not in (int, float):
raise TypeError("actual Python int or float required")
try:
converted = float(value)
except OverflowError as error:
raise ValueError("number out of range") from error
if not math.isfinite(converted) or abs(converted) > 1e6:
raise ValueError("finite magnitude at most 1e6 required")
result.append(converted)
return np.array(result, dtype=np.float64)
def system(a, b):
if type(a) not in (list, tuple) or len(a) != 2:
raise ValueError("two matrix rows required")
return np.stack([pair(row) for row in a]), pair(b)
def inspect_solve(a, b):
matrix, rhs = system(a, b)
with np.errstate(over="raise", invalid="raise", divide="raise"):
solution = np.linalg.solve(matrix, rhs)
residual = rhs - matrix @ solution
condition = float(np.linalg.cond(matrix, p=np.inf))
if not np.isfinite(solution).all() or not math.isfinite(condition):
raise ArithmeticError("nonfinite result or condition estimate")
residual_inf = float(np.max(np.abs(residual)))
rhs_inf = float(np.max(np.abs(rhs)))
return {
"solution_v": solution.tolist(),
"residual_inf_v": residual_inf,
"relative_residual_inf": None if rhs_inf == 0 else residual_inf / rhs_inf,
"condition_inf": condition,
}
def exact_two_by_two(a, b):
"""Independent Cramer oracle for supplied finite rational fixtures only."""
aa, ab = map(Fraction, a[0])
ac, ad = map(Fraction, a[1])
be, bf = map(Fraction, b)
determinant = aa * ad - ab * ac
if determinant == 0:
raise ValueError("singular exact fixture")
return ((be * ad - ab * bf) / determinant,
(aa * bf - be * ac) / determinant)
def rejects(error_type, function, *args):
try:
function(*args)
except error_type:
return True
return False
def main():
epsilon = 2.0 ** -20
matrix = [[1.0, 1.0], [1.0, 1.0 + epsilon]]
rhs = [2.0, 2.0 + epsilon]
report = inspect_solve(matrix, rhs)
exact = exact_two_by_two(matrix, rhs)
checks = {}
checks["nominal_solution_matches_exact_cramer"] = (
exact == (Fraction(1), Fraction(1))
and report["solution_v"] == [1.0, 1.0])
well = inspect_solve([[2, 0], [0, 4]], [2, 8])
checks["well_conditioned_diagonal_hand_solution"] = (
well["solution_v"] == [1.0, 2.0] and well["condition_inf"] == 2.0)
bad = np.array([2.0, 0.0]) # Deliberately wrong candidate, not solver output.
residual = np.array(rhs) - np.array(matrix) @ bad
relative_residual = float(np.max(np.abs(residual)) / max(map(abs, rhs)))
relative_forward = max(abs(Fraction(float(v)) - e) for v, e in zip(bad, exact))
relative_forward /= max(map(abs, exact))
exact_condition = (Fraction(2) + Fraction(1, 2**20)) ** 2 / Fraction(1, 2**20)
checks["deliberately_bad_candidate_has_small_residual_large_error"] = (
max(map(abs, residual)) == epsilon
and relative_residual < 5e-7 and relative_forward == 1)
checks["infinity_condition_matches_exact_inverse_row_sums"] = math.isclose(
report["condition_inf"], float(exact_condition), rel_tol=1e-14)
checks["fixed_matrix_residual_error_bound_for_exact_fixture"] = (
float(relative_forward) <= report["condition_inf"] * relative_residual)
perturbed_rhs = [2.0, 2.0 + 2 * epsilon]
perturbed = inspect_solve(matrix, perturbed_rhs)
checks["small_rhs_perturbation_has_distinct_exact_solution"] = (
exact_two_by_two(matrix, perturbed_rhs) == (Fraction(0), Fraction(2))
and perturbed["solution_v"] == [0.0, 2.0]
and perturbed["residual_inf_v"] == 0.0)
scaled = inspect_solve([[8 * v for v in row] for row in matrix],
[8 * v for v in rhs])
checks["common_power_of_two_scaling_preserves_solution_and_condition"] = (
scaled["solution_v"] == report["solution_v"]
and scaled["condition_inf"] == report["condition_inf"])
zero = inspect_solve([[1, 0], [0, 1]], [0, 0])
checks["zero_rhs_reports_absolute_residual_without_division"] = (
zero["solution_v"] == [0.0, 0.0]
and zero["residual_inf_v"] == 0.0
and zero["relative_residual_inf"] is None)
checks["singular_fixture_raises_not_silent_least_squares"] = rejects(
np.linalg.LinAlgError, inspect_solve, [[1, 1], [1, 1]], [2, 2])
checks["matrix_and_rhs_shapes_rejected"] = all(
rejects((TypeError, ValueError), inspect_solve, a, b) for a, b in [
([[1, 0]], [1, 2]), ([[1, 0, 0], [0, 1, 0]], [1, 2]),
([[1, 0], [0, 1]], [[1], [2]]), ([[1, 0], [0, 1]], [1])])
checks["coercions_nonfinite_and_bounds_rejected"] = all(
rejects((TypeError, ValueError), inspect_solve, [[1, 0], [0, 1]], [v, 1])
for v in [True, "1", complex(1), np.float64(1),
math.nan, math.inf, -math.inf, 1e6 + 1, 10**1000])
before = copy.deepcopy((matrix, rhs))
inspect_solve(matrix, rhs)
checks["inputs_are_not_mutated"] = (matrix, rhs) == before
checks["reversed_equations_preserve_solution"] = (
inspect_solve(matrix[::-1], rhs[::-1])["solution_v"] == [1.0, 1.0])
checks = {name: bool(value) for name, 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 two-channel dimensionless gain matrix",
"voltage_unit": "V", "norm": "infinity, never default 2-norm",
"epsilon": epsilon, "matrix_dimensionless": matrix, "rhs_v": rhs,
"nominal": report, "exact_nominal_solution_v": [str(v) for v in exact],
"deliberately_wrong_candidate_v": bad.tolist(),
"wrong_candidate_relative_residual_inf": relative_residual,
"wrong_candidate_relative_forward_error_inf": float(relative_forward),
"exact_condition_inf": str(exact_condition), "perturbed": perturbed,
"checks": checks, "unique_check_groups": len(checks),
}
if __name__ == "__main__":
print(json.dumps(main(), ensure_ascii=False, indent=2, allow_nan=False))
سیزده گروه بررسی اجرا شدهاند: جوابهای دستی، شاهدِ نامزد غلط، شرطِ دقیق، اغتشاش مشاهده، تغییر مقیاس مشترک، طرف راست صفر، تکینی، قرارداد نوع و شکل، حفظ ورودی و جابهجایی معادلهها. بعضی گروهها چند ورودی نامعتبر را بررسی میکنند؛ تعداد اعضای آنها را به تعداد گروههای مستقل اضافه نکردهایم. موفقیت این مجموعه اثبات همهٔ ورودیها یا کیفیت دادهٔ واقعی نیست.
۴. باقیمانده، اغتشاش ورودی و نتیجهٔ اجرا

حلگر برای دادهٔ اصلی جواب یک و یک و باقیماندهٔ صفر تولید کرد. اکنون عمداً نامزد غلطِ دو و صفر را جدا میسازیم؛ این خروجی solve نیست. باقیماندهٔ آن صفر و ε ولت است. نسبت باقیمانده حدود ۴٫۷۶۸۳۶۹ ضربدر ده به توان منفی هفت میشود، اما خطای نسبیِ جواب با مرجع معلوم برابر یک است. در نتیجه معیارِ تنها باقیمانده میتواند این نامزد بد را بپذیرد.
برای این A، نرم ماتریس دوبهعلاوهٔ ε و نرم وارون، دوبهعلاوهٔ ε تقسیم بر ε است. حاصل، حدود ۴۱۹۴۳۰۸٫۰۰۰۰۰۰۹۵۴ میشود. در حساب دقیق و برای A ثابت و b ناصفر، خطای نسبی جواب حداکثر عدد شرط ضربدر باقیماندهٔ نسبی است؛ این از رابطهٔ خطای جواب با وارون A به دست میآید. باقیماندهٔ محاسبهشدهٔ شناور نیز خطای گردکردن دارد، پس آزمون حاضر گواهی سختگیرانهٔ خطا برای هر داده نیست.
بحث هایم دربارهٔ خطای پسرو توضیح میدهد چرا حل دقیقِ مسئلهای نزدیک، لزوماً جواب نزدیکِ مسئلهٔ اولیه نیست. در شاهد دوم فقط مشاهدهٔ دوم را به اندازهٔ ε ولت افزایش میدهیم. جواب دقیقِ دادهٔ تازه صفر و دو است؛ solve همین را با باقیماندهٔ صفر میدهد. تغییر جواب در اینجا حساسیتِ داده را نشان میدهد و شاهد خرابی حلگر نیست.
{
"python_version": "3.14.5",
"numpy_version": "2.4.6",
"data_origin": "original synthetic two-channel dimensionless gain matrix",
"voltage_unit": "V",
"norm": "infinity, never default 2-norm",
"epsilon": 9.5367431640625e-07,
"matrix_dimensionless": [
[
1.0,
1.0
],
[
1.0,
1.0000009536743164
]
],
"rhs_v": [
2.0,
2.0000009536743164
],
"nominal": {
"solution_v": [
1.0,
1.0
],
"residual_inf_v": 0.0,
"relative_residual_inf": 0.0,
"condition_inf": 4194308.000000954
},
"exact_nominal_solution_v": [
"1",
"1"
],
"deliberately_wrong_candidate_v": [
2.0,
0.0
],
"wrong_candidate_relative_residual_inf": 4.76836930829558e-07,
"wrong_candidate_relative_forward_error_inf": 1.0,
"exact_condition_inf": "4398050705409/1048576",
"perturbed": {
"solution_v": [
0.0,
2.0
],
"residual_inf_v": 0.0,
"relative_residual_inf": 0.0,
"condition_inf": 4194308.000000954
},
"checks": {
"nominal_solution_matches_exact_cramer": true,
"well_conditioned_diagonal_hand_solution": true,
"deliberately_bad_candidate_has_small_residual_large_error": true,
"infinity_condition_matches_exact_inverse_row_sums": true,
"fixed_matrix_residual_error_bound_for_exact_fixture": true,
"small_rhs_perturbation_has_distinct_exact_solution": true,
"common_power_of_two_scaling_preserves_solution_and_condition": true,
"zero_rhs_reports_absolute_residual_without_division": true,
"singular_fixture_raises_not_silent_least_squares": true,
"matrix_and_rhs_shapes_rejected": true,
"coercions_nonfinite_and_bounds_rejected": true,
"inputs_are_not_mutated": true,
"reversed_equations_preserve_solution": true
},
"unique_check_groups": 13
}
عدد شرط بزرگ، حساسیت بالقوه را نشان میدهد؛ هر جهت اغتشاش الزاماً بدترین تغییر را نمیسازد. اینجا جهت مشخصی را انتخاب و جوابش را مستقل حساب کردهایم. در مسئلهٔ واقعی، جواب درست معمولاً در دسترس نیست؛ خطای نسبیِ جواب را مانند این fixture نمیتوان مستقیم گزارش کرد. باید اطلاعات ورودی، مقیاسگذاری، مدل و حدود پذیرش نیز بررسی شوند. اعتبارسنجی عددی مدار RC نمونهٔ دیگری از نقش مرجع معلوم است.
۵. چهار پرسش با پاسخ کامل

پرسش اول: اگر ε صفر شود، جواب یک و یک همچنان یکتا است؟ پاسخ: خیر؛ دو معادله یکسان و فقط مجموع دو مؤلفه معلوم میشود. زوجهای مختلفی با مجموع دو وجود دارند. این حالت با ε کوچک ولی ناصفر فرق دارد؛ در مدل دقیقِ ناصفر، جواب یکتا اما حساس است. آزمون تکینیِ کد، حالت صفر را جدا رد میکند.
پرسش دوم: چرا نامزد دو و صفر با وجود باقیماندهٔ کوچک غلط است؟ پاسخ: فاصلهٔ هر مؤلفه از مرجع یک ولت است و نرم مرجع نیز یک ولت؛ نسبت خطا یک میشود. مشاهدهٔ تقریباً تکراری، این اختلافِ پنهان را ضعیف آشکار میکند. نامزد را عمداً ساختهایم و نباید آن را نتیجهٔ عددیِ حلگر معرفی کرد.
پرسش سوم: ضرب همزمان A و b در هشت چه میکند؟ پاسخ: جواب تغییر نمیکند و عدد شرطِ همین نرم نیز ثابت میماند؛ ضریب در نرم A و وارون آن خنثی میشود. آزمون این مقیاس دودویی را اجرا میکند. تغییر مقیاس تنها یک سطر یا یک ستون، مسئلهٔ دیگری برای تحلیل نرم و تفسیر مجهولهاست.
پرسش چهارم: آیا شرط حدود چهارمیلیون، الزاماً چهارمیلیون برابر خطا میسازد؟ پاسخ: خیر؛ حد حساسیت به اندازه و جهت تغییر، تعریف نرم و مدل اغتشاش وابسته است. این درس فقط دو fixture مشخص را میسنجد. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدهاند؛ آموزش پایه است، ادعای خبر تازه، آزمایش سختافزاری، اجرای MATLAB یا اندازهگیری سرعت ندارد.
منابع و مطالعهٔ بیشتر
- NumPy 2.4؛ حل دستگاه مربع و قرارداد شکل بردار طرف راست
- NumPy 2.4؛ انتخاب صریح نرم در محاسبهٔ عدد شرط
- نیک هایم؛ تعریف حساسیت مسئله و عدد شرط
- نیک هایم؛ تفاوت خطای پسرو و خطای جواب
- Python؛ حساب کسری و حفظ مقدار واقعی float
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

