فهرست مقاله
۱. مرجعِ چندجملهای با مرزِ روشن

فرض کنید دو ماژولِ نرمافزار دربارهٔ جدولِ مرجعِ یک حسگر توافق کردهاند: یکی گرههای ولتاژ را نگه میدارد و دیگری پاسخهای جریان را تحویل میدهد. پیش از آزمونِ اتصالِ این دو، باید روشن باشد که «درونیابی» دقیقاً چه عملی است. در این درس، همهٔ گرهها یک چندجملهایِ سراسری را تعیین میکنند و خروجی، مقدارِ همان چندجملهای در یک ولتاژِ پرسوجو است. این قرارداد با درونیابیِ خطی میانِ دو گرهٔ همسایه تفاوت دارد. دادههای مثال ساختگیاند؛ نامِ حسگر به معنیِ اندازهگیری، تأییدِ کالیبراسیون یا اجرایِ آزمونِ یک سامانهٔ واقعی نیست.
برای m گرهٔ متمایز، چندجملهایِ عبوری از پاسخهای دادهشده یکتا و درجهاش حداکثر m−1 است؛ ممکن است پاسخها روی خط قرار بگیرند و درجه کمتر شود. بخشِ لاگرانژِ مرجعِ NIST این ساختار را بیان میکند. گرههای ما ۰، ۱ و ۲ ولت و پاسخها ۱، ۳ و ۲ میلیآمپرند. اگر عددِ ولتاژ را x بنامیم، چندجملهایِ عددی برابرِ ۱+(۷/۲)x−(۳/۲)x² است؛ در نیمولت، پاسخ ۱۹/۸ میلیآمپر میشود. برای همین نقطه، خطِ میانِ دو گرهٔ نخست فقط ۲ میلیآمپر میدهد؛ تفاوت، خطایِ محاسبه نیست، انتخابِ مدل است.
قراردادِ آموزشیِ ما فقط ۲ تا ۶ گره، قدرمطلقِ ولتاژ حداکثر ۸ و پاسخ حداکثر ۶۴ را میپذیرد. ورودی باید تاپلِ دقیق از int یا Fraction با مخرجِ نرمالشدهٔ حداکثرِ ۱۶ باشد. این سقفها برای کوچک و بازبینیپذیر ماندنِ مثالاند و محدودیتِ نظریِ درونیابی یا محدودیتِ SciPy نیستند.
۲. گرهٔ دقیق را پیش از تقسیم تشخیص دهیم

نمایشِ باریسنتریک، همان چندجملهای را به صورتِ نسبتِ دو جمع مینویسد: صورت، جمعِ wᵢyᵢ/(x−xᵢ) و مخرج، جمعِ wᵢ/(x−xᵢ) است. وزنِ هر گره از وارونِ حاصلضربِ اختلافِ آن گره با بقیه ساخته میشود. این دو رابطه در مستنداتِ BarycentricInterpolator و رابطهٔ وزنهای NIST آمدهاند. برای گرههای مثال، وزنها ۱/۲، منفیِ ۱ و ۱/۲ هستند. این «باریسنتریک» نامِ نمایشِ چندجملهای است؛ به معنایِ محاسبهٔ مرکزِ جرمِ یک دستگاهِ فیزیکی نیست.
در x برابرِ یکی از گرهها، عبارتِ خام شاملِ تقسیم بر صفر است، ولی پاسخِ چندجملهای کاملاً تعریف دارد: همان y ذخیرهشده. بنابراین ابتدا تمامِ پاسخها و پرسوجو را اعتبارسنجی میکنیم، سپس برابریِ دقیق با گرهها را بررسی میکنیم و تنها بیرونِ گرهها نسبت را میسازیم. در یکولت، خروجی دقیقاً ۳ است. نزدیک بودن با برابر بودن یکی نیست؛ این مثال آستانهٔ شناور یا جابهجاییِ پرسوجو با اپسیلون ندارد. Fraction برابریِ گویا را بدونِ گردکردنِ اعشاری انجام میدهد.
بیرونِ گرهها نیز صفر نبودنِ مخرج حدس نیست. جمعِ توابعِ پایهٔ لاگرانژ برابرِ یک است؛ با نوشتنِ آنها بر حسبِ حاصلضربِ گرهها، مخرجِ نسبت برابرِ وارونِ حاصلضربِ همهٔ x−xᵢ میشود. اختلافها ناصفرند، پس این مخرجِ دقیق صفر نمیشود. این استدلال دربارهٔ حسابِ گویایِ همین وزنهاست؛ تضمینِ بیخطا بودنِ محاسبهٔ شناور در نزدیکیِ گرهها نیست.
۳. وزنهای گره را با پاسخِ تازه دوباره به کار ببریم

وزنها به جایگاهِ گرهها وابستهاند، نه به پاسخهای جریان. شیء ReferenceNodes آنها را هنگامِ ساختن محاسبه و نگه میدارد. برای پاسخهای تازهٔ ۰، ۱ و ۴ روی همان گرهها، پرسوجویِ نیمولت ۱/۴ میلیآمپر میدهد؛ هندسهٔ گرهها عوض نشده است. مستنداتِ SciPy نیز امکانِ استفادهٔ دوباره از وزنها و تغییرِ پاسخها را توضیح میدهد. کدِ ما یک قراردادِ کوچکِ مستقل است؛ اجرایِ SciPy، مقایسهٔ سرعت یا کپیِ پیادهسازیِ آن را گزارش نمیکند.
گرهها لازم نیست مرتب باشند؛ هر پاسخ باید در جایگاهِ گرهٔ متناظر بماند. جابهجاییِ هر دو با یک جایگشت، همان چندجملهای را میدهد، اما مرتب کردنِ تنها گرهها قرارداد را میشکند. شیءِ ساختهشده انتسابِ معمولِ دوباره را منع میکند؛ این تصمیم برای حفظِ هندسه در برنامه است و ادعایِ حفاظت در برابرِ دستکاریِ عمدی نیست.
اگر m گره با واحدِ ولت داشته باشیم، واحدِ وزن V به توانِ منفیِ m−1 است. در نسبت، عاملهای مشترک حذف میشوند و خروجی واحدِ میلیآمپر میگیرد؛ وزنها احتمال نیستند و لازم نیست مثبت یا مجموعشان یک باشد. ضربِ همهٔ وزنها در یک ضریبِ ناصفر، پاسخِ نسبت را تغییر نمیدهد. در تبدیلِ عددیِ گرهها به ۲x+۱، وزنهای سهگرهی بر ۴ تقسیم میشوند و پرسوجویِ متناظر همان پاسخ را دارد. این آزمونِ جبری، مبدلِ خودکارِ واحدهای فیزیکی نیست.
سیاستِ این درس فقط پرسوجویِ داخلِ بازهٔ بستهٔ گرهها را میپذیرد. این تصمیم جلویِ برونیابی را میگیرد، اما تضمینِ مثبت بودن یا یکنوایی نمیدهد: پاسخهایِ ۰، ۰ و ۱ در نیمولت مقدارِ منفیِ ۱/۸ میسازند. پس «داخلِ بازه» را نباید با «میانِ پاسخهای ورودی» یکی گرفت. انتخابِ مدل، نویز و نامعلومیِ حسگر همچنان مسئلهای جداست.
۴. کدِ کامل و مرجعِ مستقیمِ لاگرانژ

کدِ کامل را در یک فایل ذخیره و با Python اجرا کنید؛ وابستگی فقط کتابخانهٔ استاندارد است. مرجعِ مستقل، مقدار را از جمعِ مستقیمِ حاصلضربهای پایهٔ لاگرانژ میسازد و از وزنها، تابعِ نسبت یا خروجیِ پیادهسازی برای پاسخِ مرجع استفاده نمیکند. رویِ همهٔ انتخابهای سهگرهی از پنج عددِ تعیینشده، پاسخهایِ محدود و پرسوجوهایِ نیمگامی، ۱۸۹۰ مورد بررسی میشود؛ این مجموعه یک گروهِ آزمون است. خانوادهٔ جداگانهٔ ۱۶۰ موردی، تکجملهایهایِ درجهٔ صفر تا m−1 را با پاسخِ تحلیلی میسنجد. تابعِ مرجعِ داخلی فرض میکند ورودیِ آزمون از پیش معتبر است؛ اعتبارسنجِ عمومیِ دیگری نیست.
"""Exact, bounded teaching contract; no SciPy or hardware execution."""
from dataclasses import dataclass, field
from fractions import Fraction as F
from itertools import combinations, permutations, product
from math import prod
import json
import platform
def _exact(value, cap, label):
if type(value) not in (int, F):
raise TypeError(label + " requires exact int or Fraction")
result = F(value)
if abs(result) > cap or result.denominator > 16:
raise ValueError(label + " exceeds the teaching bounds")
return result
def _ratio_known(nodes, values, weights, query):
"""Internal algebra for authored, valid, off-node fixtures only."""
terms = tuple(w / (query - x) for x, w in zip(nodes, weights))
return sum((t * y for t, y in zip(terms, values)), F(0)) / sum(terms, F(0))
@dataclass(frozen=True)
class ReferenceNodes:
nodes_v: tuple
weights: tuple = field(init=False)
def __post_init__(self):
if type(self.nodes_v) is not tuple:
raise TypeError("nodes must be an exact tuple")
if not 2 <= len(self.nodes_v) <= 6:
raise ValueError("use 2 through 6 nodes")
nodes = tuple(_exact(x, 8, "node V") for x in self.nodes_v)
if len(set(nodes)) != len(nodes):
raise ValueError("nodes must be distinct")
weights = tuple(
F(1) / prod((x - other for j, other in enumerate(nodes) if j != i), start=F(1))
for i, x in enumerate(nodes)
)
object.__setattr__(self, "nodes_v", nodes)
object.__setattr__(self, "weights", weights)
def evaluate(self, values_ma, query_v):
if type(values_ma) is not tuple:
raise TypeError("values must be an exact tuple")
if len(values_ma) != len(self.nodes_v):
raise ValueError("one value per node is required")
values = tuple(_exact(y, 64, "value mA") for y in values_ma)
query = _exact(query_v, 8, "query V")
if not min(self.nodes_v) <= query <= max(self.nodes_v):
raise ValueError("this profile does not extrapolate")
for x, y in zip(self.nodes_v, values):
if query == x:
return y # Before any division by query - x.
return _ratio_known(self.nodes_v, values, self.weights, query)
def direct_lagrange(nodes, values, query):
"""Independent product-basis oracle for valid, small authored fixtures."""
answer = F(0)
for j, y in enumerate(values):
basis = F(1)
for k, other in enumerate(nodes):
if k != j:
basis *= (query - other) / (nodes[j] - other)
answer += y * basis
return answer
def _rejects(action, expected):
try:
action()
except expected:
return True
return False
def run():
checks = {}
counts = {}
def check(name, condition):
if type(condition) is not bool or not condition:
raise AssertionError(name)
checks[name] = condition
ref = ReferenceNodes((0, 1, 2))
values = (1, 3, 2)
query = F(1, 2)
main = ref.evaluate(values, query)
check("literal_weights_and_hand_polynomial_value", ref.weights == (F(1, 2), F(-1), F(1, 2)) and main == F(19, 8))
check("every_exact_node_and_endpoint_precedes_ratio_division", all(ref.evaluate(values, x) == F(y) for x, y in zip(ref.nodes_v, values)) and _rejects(lambda: _ratio_known(ref.nodes_v, values, ref.weights, F(1)), ZeroDivisionError))
before = ref.weights
reused = ref.evaluate((0, 1, 4), query)
check("same_geometry_reuses_unchanged_weights_for_new_values", reused == F(1, 4) and ref.weights is before and ref.evaluate((5, 5, 5), query) == 5)
exhaustive = 0
for node_tuple in combinations((-2, -1, 0, 1, 2), 3):
geometry = ReferenceNodes(node_tuple)
for ys in product((-1, 0, 2), repeat=3):
for half in range(2 * min(node_tuple), 2 * max(node_tuple) + 1):
q = F(half, 2)
assert geometry.evaluate(ys, q) == direct_lagrange(geometry.nodes_v, tuple(map(F, ys)), q)
exhaustive += 1
counts["three_node_direct_oracle_cases_one_family"] = exhaustive
check("all_bounded_three_node_fixtures_match_independent_lagrange_basis", exhaustive > 0)
polynomial_cases = 0
for n in range(2, 7):
nodes = tuple(F(i) for i in range(-2, -2 + n))
geometry = ReferenceNodes(nodes)
# abs(x) <= 3, degree <= 5: abs(x**degree / 4) <= 243/4.
for degree in range(n):
ys = tuple(x ** degree / 4 for x in nodes)
for twice in range(2 * int(min(nodes)), 2 * int(max(nodes)) + 1):
q = F(twice, 2)
wanted = q ** degree / 4
assert geometry.evaluate(ys, q) == wanted
polynomial_cases += 1
counts["analytic_monomial_degree_up_to_n_minus_1_cases_one_family"] = polynomial_cases
check("known_polynomials_through_maximum_degree_are_reproduced", polynomial_cases > 0)
shifted = ReferenceNodes(tuple(2 * x + 1 for x in ref.nodes_v))
check("affine_node_change_preserves_values_and_predicts_weight_units", shifted.evaluate(values, 2 * query + 1) == main and shifted.weights == tuple(w / 4 for w in ref.weights))
check("common_nonzero_weight_scale_cancels_in_the_ratio", all(_ratio_known(ref.nodes_v, tuple(map(F, values)), tuple(c * w for w in ref.weights), query) == main for c in (F(-3), F(1, 7), F(5))))
check("affine_response_change_is_preserved", ref.evaluate(tuple(2 * y - 3 for y in values), query) == 2 * main - 3)
permutation_cases = 0
for order in permutations(range(3)):
permuted = ReferenceNodes(tuple(ref.nodes_v[i] for i in order))
assert permuted.evaluate(tuple(values[i] for i in order), query) == main
permutation_cases += 1
counts["joint_node_value_permutations_one_family"] = permutation_cases
check("paired_permutation_preserves_the_same_interpolant", permutation_cases == 6)
six = ReferenceNodes((-8, -4, -1, 1, 4, 8))
check("inclusive_six_node_and_value_bounds_are_valid", all(six.evaluate((64,) * 6, q) == 64 for q in (-8, 0, 8)))
overshoot = ref.evaluate((0, 0, 1), query)
check("inside_hull_does_not_guarantee_nonnegative_output", overshoot == F(-1, 8))
bad_queries = ((True, TypeError), (0.5, TypeError), (float("nan"), TypeError), ("1", TypeError), (F(1, 17), ValueError), (-1, ValueError), (3, ValueError), (9, ValueError))
check("query_exact_type_denominator_and_no_extrapolation_contract", all(_rejects(lambda q=q: ref.evaluate(values, q), error) for q, error in bad_queries))
bad_nodes = (([0, 1], TypeError), ((), ValueError), ((0,), ValueError), (tuple(range(7)), ValueError), ((0, F(0), 1), ValueError), ((0, True), TypeError), ((0, 1.0), TypeError), ((0, F(1, 17)), ValueError), ((0, 9), ValueError))
check("node_container_count_distinctness_and_exact_bounds", all(_rejects(lambda xs=xs: ReferenceNodes(xs), error) for xs, error in bad_nodes))
bad_values = (([1, 3, 2], TypeError), ((1, 2), ValueError), ((1, 3, True), TypeError), ((1, 3, 2.0), TypeError), ((1, 3, F(1, 17)), ValueError), ((1, 3, 65), ValueError), ((1, 3, float("inf")), TypeError))
check("all_response_values_validated_even_at_an_exact_node", all(_rejects(lambda ys=ys: ref.evaluate(ys, 0), error) for ys, error in bad_values))
assert len(checks) == 14
return {
"python": platform.python_version(),
"dependencies": "standard library only; SciPy not executed",
"profile": {"nodes": "exact tuple 2..6, abs V <= 8", "values": "same-size exact tuple, abs mA <= 64", "scalars": "exact int/Fraction only, normalized denominator <= 16", "query": "within closed node hull; no extrapolation"},
"main": {"nodes_v": [str(x) for x in ref.nodes_v], "values_ma": list(values), "weights_v_power_minus_2": [str(w) for w in ref.weights], "query_v": str(query), "interpolated_ma": str(main), "exact_node_1_v_ma": str(ref.evaluate(values, 1)), "reused_squared_response_ma": str(reused), "nonnegative_data_overshoot_ma": str(overshoot)},
"case_counts_not_additional_groups": counts,
"checks": checks,
"unique_checks": len(checks),
"all_passed": all(checks.values()),
"limits": "Exact small rational fixtures only; no float stability, timing, physical calibration truth, monotonicity or fitting claim. Internal oracles assume valid authored inputs."
}
if __name__ == "__main__":
print(json.dumps(run(), ensure_ascii=True, indent=2))
خروجیِ ثبتشده از یک اجرایِ کامل با Python 3.14.5، چهارده گروهِ نامدارِ موفق و خطایِ استانداردِ خالی دارد. جایگشتِ همزمانِ گره و پاسخ، تبدیلِ افاینِ پاسخ، تغییرِ مقیاسِ وزن، مرزِ ششگرهی و ورودیهایِ نامعتبر جدا بررسی میشوند. bool، عددِ شناور حتی اگر ظاهراً صحیح باشد، NaN، بینهایت و تبدیلِ ضمنی پذیرفته نمیشوند. نامعتبر بودنِ یک پاسخ حتی وقتی پرسوجو دقیقاً رویِ گرهٔ دیگری است نیز رد میشود. نسخهٔ مستنداتِ SciPy در زمانِ دسترسی 1.18.0 بود؛ کتابخانه در این آزمایش اجرا نشده است. این آزمونها شاهدهایِ محدود و قابلِ تکرارند، نه اثباتِ برنامه برای هر ورودی.
منابع در ۱۹ اکتبرِ ۲۰۲۶ خوانده شدند. انتشارِ نسخهٔ 1.2.8 مرجعِ DLMF در ۱۵ سپتامبرِ ۲۰۲۶ است؛ این تاریخ را تاریخِ نخستین انتشارِ فرمولها نمینامیم. رابطههایِ خطایِ درونیابی فرضهایِ مشتقپذیری میخواهند؛ چون تابعِ واقعیِ حسگر را نداریم، از آنها کرانِ خطایِ اندازهگیری استخراج نمیکنیم.
{
"python": "3.14.5",
"dependencies": "standard library only; SciPy not executed",
"profile": {
"nodes": "exact tuple 2..6, abs V <= 8",
"values": "same-size exact tuple, abs mA <= 64",
"scalars": "exact int/Fraction only, normalized denominator <= 16",
"query": "within closed node hull; no extrapolation"
},
"main": {
"nodes_v": [
"0",
"1",
"2"
],
"values_ma": [
1,
3,
2
],
"weights_v_power_minus_2": [
"1/2",
"-1",
"1/2"
],
"query_v": "1/2",
"interpolated_ma": "19/8",
"exact_node_1_v_ma": "3",
"reused_squared_response_ma": "1/4",
"nonnegative_data_overshoot_ma": "-1/8"
},
"case_counts_not_additional_groups": {
"three_node_direct_oracle_cases_one_family": 1890,
"analytic_monomial_degree_up_to_n_minus_1_cases_one_family": 160,
"joint_node_value_permutations_one_family": 6
},
"checks": {
"literal_weights_and_hand_polynomial_value": true,
"every_exact_node_and_endpoint_precedes_ratio_division": true,
"same_geometry_reuses_unchanged_weights_for_new_values": true,
"all_bounded_three_node_fixtures_match_independent_lagrange_basis": true,
"known_polynomials_through_maximum_degree_are_reproduced": true,
"affine_node_change_preserves_values_and_predicts_weight_units": true,
"common_nonzero_weight_scale_cancels_in_the_ratio": true,
"affine_response_change_is_preserved": true,
"paired_permutation_preserves_the_same_interpolant": true,
"inclusive_six_node_and_value_bounds_are_valid": true,
"inside_hull_does_not_guarantee_nonnegative_output": true,
"query_exact_type_denominator_and_no_extrapolation_contract": true,
"node_container_count_distinctness_and_exact_bounds": true,
"all_response_values_validated_even_at_an_exact_node": true
},
"unique_checks": 14,
"all_passed": true,
"limits": "Exact small rational fixtures only; no float stability, timing, physical calibration truth, monotonicity or fitting claim. Internal oracles assume valid authored inputs."
}
۵. چهار پرسش دربارهٔ درجه و مرزِ تفسیر

پرسشِ اول: برای گرههایِ ۰، ۱ و ۲ و پاسخهایِ ۱، ۳ و ۲، نیمولت چه پاسخی دارد؟ پاسخ: جایگذاری در چندجملهایِ بخشِ اول، ۱+۷/۴−۳/۸، یعنی ۱۹/۸ میلیآمپر میدهد. پاسخِ خطیِ ۲ متعلق به قراردادِ دیگری است.
پرسشِ دوم: در یکولت باید نسبتِ خام را محاسبه کنیم؟ پاسخ: خیر؛ پس از اعتبارسنجیِ کامل، شاخهٔ برابری مقدارِ ۳ را مستقیماً برمیگرداند. تقسیم بر صفرِ نمایشِ خام مجوزِ تغییر دادنِ گره یا حذفِ پاسخ نیست.
پرسشِ سوم: اگر فقط پاسخها به ۰، ۱ و ۴ تغییر کنند چه چیزی دوباره ساخته میشود؟ پاسخ: بردارِ پاسخِ تازه اعتبارسنجی میشود، ولی وزنهایِ همان شیء حفظ میشوند. چندجملهای اکنون x² و مقدارِ نیمولت ۱/۴ میلیآمپر است؛ هیچ نتیجهٔ زمانی از این استفادهٔ دوباره استنتاج نمیکنیم.
پرسشِ چهارم: آیا محدود کردنِ پرسوجو به بازه، پاسخِ غیرمنفی را تضمین میکند؟ پاسخ: خیر؛ شاهدِ ۰، ۰ و ۱ پاسخِ منفیِ ۱/۸ میدهد. همچنین سه گره همیشه درجهٔ دقیقِ دو ایجاد نمیکنند؛ پاسخِ ثابت درجهٔ صفر دارد. درسِ Simpson با گامِ نامساوی از همین ریاضیاتِ پایه برای انتگرالِ محلی استفاده میکند؛ اینجا هدف، مقدارِ نقطهای و قراردادِ مشترکِ ماژولهاست، نه انتگرال، برازشِ نویز یا اثباتِ حقیقتِ فیزیکیِ جدول.
منابع و مطالعهٔ بیشتر
- SciPy1.18.0؛ نمایش باریسنتریک و استفادهٔ دوباره از وزنها
- NIST DLMF3.3(i)؛ پایهٔ لاگرانژ، وزنها و جمعِ واحد
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

