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

۱. معادلهٔ مدار و مرجع مستقل یک ولت

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

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

جریان مقاومت برحسب میلی‌آمپر، از «دو منهای ولتاژ بار» تقسیم بر یک کیلواهم به دست می‌آید. تعادل جریان، برابر شدن جریان ورودی و جریان بار را لازم می‌کند. اصل حفظ جریان و افت مقاومتی در بخش قوانین کیرشهف OpenStax توضیح داده شده است؛ انتخاب بار مربعی و اعداد، متعلق به همین مثال‌اند.

باقیمانده را «جریان بار منهای جریان مقاومت» می‌گیریم. با جای‌گذاری مقدار عددی V برحسب ولت، عبارت عددی آن V*V+V-2 برحسب میلی‌آمپر می‌شود. تجزیهٔ مستقل آن (V-1)*(V+2) است؛ ریشهٔ داخل بازهٔ صفر تا ۱٫۵ ولت، یک ولت و جریان متناظر یک میلی‌آمپر است. ریشهٔ منفی دو بیرون دامنهٔ انتخابی ما قرار دارد. این جواب را حل‌گر دیگری تولید نکرده است.

۲. bracket معتبر چه فرضی دارد؟

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

در صفر ولت، باقیمانده منفی دو و در ۱٫۵ ولت مثبت ۱٫۷۵ میلی‌آمپر است. در روش نصف‌کردن، نقطهٔ میانی بررسی و نیمه‌ای حفظ می‌شود که علامت دو انتهایش مخالف است. بخش روش نصف‌کردن DLMF چارچوب بازه‌ای را شرح می‌دهد و مستند bisect در SciPy پیوستگی تابع را صریحاً شرط می‌داند. کد حاضر پیاده‌سازی آموزشی مستقل است و SciPy را اجرا نمی‌کند.

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

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

۳. تلورانس ولتاژ را با باقیماندهٔ جریان یکی نگیریم

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

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

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

قرارداد ما تلورانس مطلق ولتاژ دارد و معادل تمام جزئیات xtol و rtol در API کتابخانه‌ها نیست. سه علت موفقیت جدا ثبت می‌شوند: صفر محاسبه‌شدهٔ انتها، صفر محاسبه‌شدهٔ میانه یا رسیدن شعاع بازه به تلورانس. صفر شناور به‌تنهایی گواه عمومی صفر ریاضی نیست؛ تفسیر کران به پیوستگی، علامت معتبر و مدل وابسته است. این برنامه حساب بازه‌ایِ تضمین‌شده پیاده نمی‌کند.

۴. کد کامل و نتیجهٔ هجده بررسی

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

فایل bisection_circuit.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. کران‌ها و تلورانس باید دقیقاً int یا float متناهی باشند؛ bool پذیرفته نیست. طول بازه نیز باید متناهی و مثبت باشد و بودجهٔ تکرار عدد صحیح مثبت است. خروجی نامتناهی یا نوع نادرست تابع خطا می‌دهد. بودجهٔ ناکافی هم به جای بازگرداندن یک عدد با برچسب موفقیت، خطا ایجاد می‌کند.

"""Authored nonlinear DC model and bracket-contract tests; no circuit measurement."""
from fractions import Fraction
import json
import math
import platform


def finite_number(value):
    if type(value) not in (int, float):
        raise TypeError('Exact int or float required; bool excluded')
    try:
        number = float(value)
    except OverflowError as error:
        raise ValueError('Finite float representation required') from error
    if not math.isfinite(number):
        raise ValueError('Finite number required')
    return number


def bisect_voltage(function, left_v, right_v, xtol_v, max_iterations=64):
    if not callable(function):
        raise TypeError('Callable scalar function required')
    left, right, tolerance = map(finite_number, (left_v, right_v, xtol_v))
    if left >= right or not math.isfinite(right-left):
        raise ValueError('Ordered finite-span bracket required')
    if tolerance <= 0:
        raise ValueError('Positive voltage tolerance required')
    if type(max_iterations) is not int or max_iterations < 1:
        raise ValueError('Positive exact integer iteration budget required')

    def evaluate(voltage):
        return finite_number(function(voltage))

    left_f, right_f = evaluate(left), evaluate(right)

    def result(root, value, reason, iterations, trace, low, high):
        return {'root_v': root, 'residual_ma': value, 'reason': reason,
                'iterations': iterations, 'left_v': low, 'right_v': high,
                'radius_v': max(root-low, high-root), 'trace': trace}

    if left_f == 0.0:
        return result(left, left_f, 'computed_left_zero', 0, [], left, left)
    if right_f == 0.0:
        return result(right, right_f, 'computed_right_zero', 0, [], right, right)
    # Compare signs without multiplying values, which could underflow or overflow.
    if (left_f > 0) == (right_f > 0):
        raise ValueError('Opposite nonzero endpoint signs required')
    trace = []
    for iteration in range(1, max_iterations + 1):
        midpoint = left + (right-left) / 2.0
        radius = max(midpoint-left, right-midpoint)
        if (midpoint == left or midpoint == right) and radius > tolerance:
            raise ArithmeticError('No distinct representable midpoint before requested tolerance')
        mid_f = evaluate(midpoint)
        trace.append({'left_v': left, 'right_v': right, 'midpoint_v': midpoint,
                      'left_f_ma': left_f, 'right_f_ma': right_f,
                      'mid_f_ma': mid_f, 'radius_v': radius})
        if mid_f == 0.0:
            return result(midpoint, mid_f, 'computed_midpoint_zero', iteration, trace, midpoint, midpoint)
        if radius <= tolerance:
            return result(midpoint, mid_f, 'bracket_radius', iteration, trace, left, right)
        if (left_f > 0) == (mid_f > 0):
            left, left_f = midpoint, mid_f
        else:
            right, right_f = midpoint, mid_f
    raise RuntimeError('Iteration budget exhausted without meeting a stopping rule')


def residual_ma(voltage_v):
    # Authored load: 1 mA/V^2 times V^2; source2V through resistance1kohm.
    load_current_ma = 1.0 * voltage_v**2
    resistor_current_ma = (2.0-voltage_v) / 1.0
    return load_current_ma - resistor_current_ma


def rejects(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    tolerance_v = 1e-6
    answer = bisect_voltage(residual_ma, 0, 1.5, tolerance_v)
    trace = answer['trace']
    # Independent exact algebra: v^2+v-2=(v-1)(v+2), physical bracket root1V.
    exact_root_v = 1.0
    scaled = bisect_voltage(lambda v: 1e-200 * residual_ma(v), 0, 1.5, tolerance_v)
    left_zero = bisect_voltage(residual_ma, 1, 1.5, tolerance_v)
    right_zero = bisect_voltage(residual_ma, 0, 1, tolerance_v)
    midpoint_zero = bisect_voltage(residual_ma, 0, 2, tolerance_v)
    adjacent_left = 1.0
    adjacent_right = math.nextafter(adjacent_left, math.inf)
    exact_between = (Fraction.from_float(adjacent_left) + Fraction.from_float(adjacent_right)) / 2

    def between_function(v):
        # Artificial linear diagnostic with numerical slope1mA/V, not the load.
        return float(Fraction.from_float(v) - exact_between)

    checks = {
        'root_agrees_with_independent_exact_one_volt_reference': abs(answer['root_v']-exact_root_v) <= tolerance_v,
        'returned_bracket_still_contains_analytic_root': answer['left_v'] <= exact_root_v <= answer['right_v'],
        'declared_radius_covers_actual_error_and_meets_voltage_tolerance': abs(answer['root_v']-exact_root_v) <= answer['radius_v'] <= tolerance_v
            and answer['reason'] == 'bracket_radius',
        'every_trace_bracket_preserves_opposite_signs_and_root': all(
            row['left_v'] <= exact_root_v <= row['right_v']
            and row['left_f_ma'] < 0 < row['right_f_ma'] for row in trace),
        'successive_authored_bracket_widths_halve': all(
            next_row['right_v']-next_row['left_v'] == (row['right_v']-row['left_v'])/2
            for row, next_row in zip(trace, trace[1:])),
        'first_two_midpoints_and_residuals_match_hand_arithmetic': trace[0]['midpoint_v'] == 0.75
            and trace[0]['mid_f_ma'] == -0.6875 and trace[1]['midpoint_v'] == 1.125
            and trace[1]['mid_f_ma'] == 0.390625,
        'left_endpoint_computed_zero_has_zero_midpoint_iterations': left_zero['root_v'] == 1.0
            and left_zero['iterations'] == 0 and left_zero['reason'] == 'computed_left_zero',
        'right_endpoint_computed_zero_has_zero_midpoint_iterations': right_zero['root_v'] == 1.0
            and right_zero['iterations'] == 0 and right_zero['reason'] == 'computed_right_zero',
        'exact_interior_computed_zero_is_distinguished_from_width_stop': midpoint_zero['root_v'] == 1.0
            and midpoint_zero['iterations'] == 1 and midpoint_zero['reason'] == 'computed_midpoint_zero',
        'tiny_residual_scaling_preserves_same_voltage_decisions': scaled['root_v'] == answer['root_v']
            and scaled['left_v'] == answer['left_v'] and scaled['right_v'] == answer['right_v']
            and scaled['iterations'] == answer['iterations'],
        'small_current_residual_alone_does_not_imply_small_voltage_error': abs(1e-200*residual_ma(0.0)) < 1e-6
            and abs(0.0-exact_root_v) == 1.0,
        'unordered_nonbracketing_and_even_root_without_sign_change_rejected': all(rejects(call, ValueError) for call in (
            lambda: bisect_voltage(residual_ma, 1.5, 0, tolerance_v),
            lambda: bisect_voltage(residual_ma, 0, 0, tolerance_v),
            lambda: bisect_voltage(residual_ma, 1.2, 1.5, tolerance_v),
            lambda: bisect_voltage(lambda v: v*v, -1, 1, tolerance_v))),
        'invalid_voltage_tolerance_is_rejected': all(
            rejects(lambda v=v: bisect_voltage(residual_ma, 0, 1.5, v), (TypeError, ValueError))
            for v in (True, '1e-6', 0, -1, math.nan, math.inf)),
        'invalid_iteration_budget_is_rejected': all(
            rejects(lambda n=n: bisect_voltage(residual_ma, 0, 1.5, tolerance_v, n), ValueError)
            for n in (True, 0, -1, 2.0)),
        'nonfinite_bounds_and_function_values_rejected': rejects(lambda: bisect_voltage(residual_ma, 0, math.inf, tolerance_v), ValueError)
            and rejects(lambda: bisect_voltage(residual_ma, 0, 10**400, tolerance_v), ValueError)
            and all(rejects(lambda value=value: bisect_voltage(lambda v: value, 0, 1.5, tolerance_v), (TypeError, ValueError))
                    for value in (math.nan, math.inf, True)),
        'insufficient_iteration_budget_reports_failure': rejects(lambda: bisect_voltage(residual_ma, 0, 1.5, tolerance_v, 2), RuntimeError),
        'adjacent_float_bracket_reports_stagnation_before_tolerance': between_function(adjacent_left) < 0 < between_function(adjacent_right)
            and rejects(lambda: bisect_voltage(between_function, adjacent_left, adjacent_right, 1e-18), ArithmeticError),
        'finite_endpoints_with_overflowing_span_are_rejected': rejects(lambda: bisect_voltage(lambda v: v, -1e308, 1e308, tolerance_v), ValueError),
    }
    assert len(checks) == 18 and all(type(value) is bool for value in checks.values())
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'dependencies': 'standard library only',
            'scope': 'authored continuous nonlinear DC model, no circuit measurements or SciPy execution',
            'model': {'source_v': 2.0, 'resistance_kohm': 1.0,
                      'load_coefficient_ma_per_v2': 1.0,
                      'residual': 'load_current_ma-resistor_current_ma',
                      'analytic_factorization': '(v-1)*(v+2) in declared numerical V/mA scales',
                      'physical_interval_root_v': exact_root_v, 'root_current_ma': 1.0},
            'initial_bracket_v': [0.0, 1.5], 'xtol_v': tolerance_v,
            'solution': {key: value for key, value in answer.items() if key != 'trace'},
            'first_two_trace_rows': trace[:2],
            'analytic_absolute_voltage_error_v': abs(answer['root_v']-exact_root_v),
            'scaled_residual_probe': {'multiplier': 1e-200, 'root_v': scaled['root_v'],
                                      'residual_at_wrong_zero_volt': 1e-200*residual_ma(0)},
            'adjacent_float_stagnation_probe': {'left_v': adjacent_left, 'right_v': adjacent_right,
                                               'tolerance_v': 1e-18, 'outcome': 'ArithmeticError'},
            'zero_stop_note': 'Computed floating zero only; general guarantee also needs reliable signs and continuity',
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), indent=2))

جواب ثبت‌شده حدود ۰٫۹۹۹۹۹۹۷۶۱۵۸۱۴۲۱ ولت است. خطای واقعی نسبت به مرجع یک ولت، حدود ۰٫۲۳۸۴۱۹ میکروولت و شعاع گزارش‌شده حدود ۰٫۷۱۵۲۵۶ میکروولت شد. هر دو از تلورانس یک میکروولت کوچک‌ترند. باقیمانده حدود منفی ۰٫۷۱۵۲۵۶ نانوآمپر است؛ خروجی JSON آن را برحسب میلی‌آمپر نگه می‌دارد. این واحدها شاهد دقت وسیلهٔ واقعی نیستند.

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

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "authored continuous nonlinear DC model, no circuit measurements or SciPy execution",
  "model": {
    "source_v": 2.0,
    "resistance_kohm": 1.0,
    "load_coefficient_ma_per_v2": 1.0,
    "residual": "load_current_ma-resistor_current_ma",
    "analytic_factorization": "(v-1)*(v+2) in declared numerical V/mA scales",
    "physical_interval_root_v": 1.0,
    "root_current_ma": 1.0
  },
  "initial_bracket_v": [
    0.0,
    1.5
  ],
  "xtol_v": 1e-06,
  "solution": {
    "root_v": 0.9999997615814209,
    "residual_ma": -7.152556804612686e-07,
    "reason": "bracket_radius",
    "iterations": 21,
    "left_v": 0.9999990463256836,
    "right_v": 1.0000004768371582,
    "radius_v": 7.152557373046875e-07
  },
  "first_two_trace_rows": [
    {
      "left_v": 0.0,
      "right_v": 1.5,
      "midpoint_v": 0.75,
      "left_f_ma": -2.0,
      "right_f_ma": 1.75,
      "mid_f_ma": -0.6875,
      "radius_v": 0.75
    },
    {
      "left_v": 0.75,
      "right_v": 1.5,
      "midpoint_v": 1.125,
      "left_f_ma": -0.6875,
      "right_f_ma": 1.75,
      "mid_f_ma": 0.390625,
      "radius_v": 0.375
    }
  ],
  "analytic_absolute_voltage_error_v": 2.384185791015625e-07,
  "scaled_residual_probe": {
    "multiplier": 1e-200,
    "root_v": 0.9999997615814209,
    "residual_at_wrong_zero_volt": -2e-200
  },
  "adjacent_float_stagnation_probe": {
    "left_v": 1.0,
    "right_v": 1.0000000000000002,
    "tolerance_v": 1e-18,
    "outcome": "ArithmeticError"
  },
  "zero_stop_note": "Computed floating zero only; general guarantee also needs reliable signs and continuity",
  "checks": {
    "root_agrees_with_independent_exact_one_volt_reference": true,
    "returned_bracket_still_contains_analytic_root": true,
    "declared_radius_covers_actual_error_and_meets_voltage_tolerance": true,
    "every_trace_bracket_preserves_opposite_signs_and_root": true,
    "successive_authored_bracket_widths_halve": true,
    "first_two_midpoints_and_residuals_match_hand_arithmetic": true,
    "left_endpoint_computed_zero_has_zero_midpoint_iterations": true,
    "right_endpoint_computed_zero_has_zero_midpoint_iterations": true,
    "exact_interior_computed_zero_is_distinguished_from_width_stop": true,
    "tiny_residual_scaling_preserves_same_voltage_decisions": true,
    "small_current_residual_alone_does_not_imply_small_voltage_error": true,
    "unordered_nonbracketing_and_even_root_without_sign_change_rejected": true,
    "invalid_voltage_tolerance_is_rejected": true,
    "invalid_iteration_budget_is_rejected": true,
    "nonfinite_bounds_and_function_values_rejected": true,
    "insufficient_iteration_budget_reports_failure": true,
    "adjacent_float_bracket_reports_stagnation_before_tolerance": true,
    "finite_endpoints_with_overflowing_span_are_rejected": true
  }
}

۵. چهار تمرین پاسخ‌دار و توقف در مرز float

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

تمرین اول: اولین نقطهٔ میانی و باقیمانده چیست؟ پاسخ: ۰٫۷۵ ولت و منفی ۰٫۶۸۷۵ میلی‌آمپر؛ پس نیمهٔ صفر تا ۰٫۷۵ کنار می‌رود. نقطهٔ بعدی ۱٫۱۲۵ ولت و باقیمانده‌اش مثبت ۰٫۳۹۰۶۲۵ است. این دو گام در آزمون دستی بررسی می‌شوند.

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

تمرین سوم: اگر بودجه فقط دو تکرار باشد چه می‌شود؟ پاسخ: اجرای آزموده‌شده RuntimeError می‌دهد؛ آخرین تخمین بدون رسیدن به قرارداد موفق اعلام نمی‌شود. افزایش بودجه نیز جای bracket درست یا پیوستگی را نمی‌گیرد.

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

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

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