Chapter 2
15 min read
Section 16 of 353

Limits at Infinity: Horizontal Asymptotes

Limits - Approaching the Infinite

Learning Objectives

By the end of this section, you will be able to:

  1. Interpret limxf(x)=L\lim_{x \to \infty} f(x) = L as a statement about settling, not arrival.
  2. Recognize a horizontal asymptote on a graph and explain what it signals about end-behavior.
  3. Apply the divide-by-highest-power trick to compute limits of rational functions.
  4. Use the degree race (numerator vs. denominator) to decide between a finite asymptote, zero, and infinity.
  5. Prove a limit at infinity using the formal ε–N definition.
  6. Translate every technique into Python and PyTorch to verify results numerically.

The Question: What Happens at the Far End?

So far our limits have all asked “what value does f(x)f(x) settle on as xx closes in on some finite point?” Now we change the horizon. We ask what happens when xx is allowed to run away — unboundedly — toward ++\infty or -\infty. The machinery from one-sided limits carries over almost word-for-word; only the moving part changes.

The motivating picture: imagine releasing a skydiver at t=0t = 0. Air resistance grows with speed, so the falling object does not keep accelerating forever. Instead, speed settles to a terminal velocity. The function v(t)v(t) has a horizontal asymptote, and the language of “tt \to \infty” limits is what lets us name and compute that terminal value.

Intuition: Settling Down to a Level

Write limxf(x)=L\lim_{x \to \infty} f(x) = L and read it out loud: “as xx is made larger and larger, the value f(x)f(x) gets trapped arbitrarily close to LL and stays there.” Three features of that sentence matter:

  • Arbitrarily close, not equal. The function never has to reach LL. It just has to refuse to leave any tolerance band around LL once xx is big enough.
  • Eventually, not immediately. The function is allowed to behave wildly for small xx. We only care about what it does past some cut-off.
  • And stays. Once trapped in the band, the function must not escape. A graph that dips in and out of a horizontal strip does not have a limit at infinity.

Interactive: Watching a Limit Form

Slide the x-window to the right and the tolerance band (ε) tighter. Notice how you can always squeeze the band smaller — and for any such ε you can still find an x-value past which the curve never leaves the band. That is exactly what “limit at infinity” means, in visual form.

Horizontal Asymptote Explorer

Drag the x-window and ε-tube to see how f(x) settles toward L.

y = L = 3.0000f(20.0) = 3.23130.05.0101520
x-window maximum: 20(push x toward ∞)
ε (tolerance band): 0.200tighten to test the limit
Try each of the four preset functions. Note that arctan(x)\arctan(x) asymptotes to π/21.5708\pi/2 \approx 1.5708, and that 1+sin(x)/x1 + \sin(x)/x oscillates but is still squeezed into the band by the amplitude 1/x01/x \to 0.

The Horizontal Asymptote

When the limit exists, the horizontal line y=Ly = L is called the horizontal asymptote of ff. Formally:

limx±f(x)=L    y=L is a horizontal asymptote.\lim_{x \to \pm\infty} f(x) = L \iff y = L \text{ is a horizontal asymptote.}

A function can have up to two horizontal asymptotes: one for x+x \to +\infty and a potentially different one for xx \to -\infty. The arctangent is the classic example: arctan(x)π/2\arctan(x) \to \pi/2 at ++\infty, π/2-\pi/2 at -\infty.

Horizontal vs. vertical asymptote

A vertical asymptote (next section) says x approaches a finite point and y blows up. A horizontal asymptote says x blows up and y settles. They are mirror images of one another.

The Formal Definition (ε–N)

The informal “arbitrarily close, eventually” language becomes mathematics when we ask: how close, and past which x? The answer is a contract:

ε–N definition

limxf(x)=L\displaystyle \lim_{x \to \infty} f(x) = L means:

For every ε>0\varepsilon > 0 there exists N>0N > 0 such that x>N    f(x)L<εx > N \implies |f(x) - L| < \varepsilon.

Read it as a negotiation. A skeptic hands you a tolerance ε\varepsilon. You must produce a cut-off NN that depends on ε\varepsilon and guarantees the function stays inside the ε\varepsilon-band past NN. If you can answer any ε\varepsilon, the limit exists.

Worked ε–N proof

Let f(x)=3x+1x+2f(x) = \dfrac{3x + 1}{x + 2}. We claim limxf(x)=3\lim_{x\to\infty} f(x) = 3. Let ε>0\varepsilon > 0.

3x+1x+23=3x+13(x+2)x+2=5x+2.\left| \frac{3x+1}{x+2} - 3 \right| = \left| \frac{3x+1 - 3(x+2)}{x+2} \right| = \frac{5}{x+2}.

We need 5x+2<ε\dfrac{5}{x+2} < \varepsilon, i.e. x>5ε2x > \dfrac{5}{\varepsilon} - 2. So pick N=max ⁣(5ε2,  0)N = \max\!\left(\dfrac{5}{\varepsilon} - 2,\;0\right). For any challenge ε\varepsilon, this explicit NN works. The limit is proved. ∎


Interactive: The ε–N Challenge

Be the skeptic, then the responder. Shrink ε; you can always win by pushing N big enough. The algebraic formula N=5/ε2N = 5/\varepsilon - 2 is the universal winning strategy — the proof in action.

The ε–N Game: Challenge and Response

Skeptic picks ε. You must find an N so that |f(x) − L| < ε for every x > N. Function: f(x) = (3x+1)/(x+2), L = 3.

N = 18.0L = 3015304560
ε (skeptic's tolerance): 0.250
N (your response): 18.0need N ≥ 18.00

The Three Fates of a Rational Function

For a rational function f(x)=P(x)Q(x)f(x) = \dfrac{P(x)}{Q(x)} with degP=m\deg P = m and degQ=n\deg Q = n, end-behavior is settled entirely by the degree race:

CaseBehavior as x → ∞Horizontal asymptote?
m < n (denominator wins)f(x) → 0Yes, y = 0
m = n (tied)f(x) → (leading coeff of P) / (leading coeff of Q)Yes, y = a_m / b_n
m > n (numerator wins)|f(x)| → ∞No — there may be a slant / curved asymptote instead

The mental trick: divide top and bottom by the highest power of x anywhere in sight. Every term like 5/x5/x or 1/x21/x^2 vanishes, and the picture becomes transparent.

Why the trick works

The identity P(x)Q(x)=P(x)/xnQ(x)/xn\dfrac{P(x)}{Q(x)} = \dfrac{P(x)/x^n}{Q(x)/x^n} is algebraic — true for every x0x \neq 0. The limit is then a limit of two polynomials in 1/x1/x, and 1/x01/x \to 0 as xx \to \infty kills every term except the constant.

Interactive: Degree Race

Three rational functions of the same family are plotted below. Watch how each settles — one to 0, one to 3, one unbounded — as you push the x-window further right.

The Three Fates of a Rational Function

Only three things can happen to P(x)/Q(x) as x → ∞ — and all three depend on the degree race.

0.2543.23119.8040.05.0101520-0.50.03.020.6
x-window: 20

Worked Example — Step by Step

Find limx3x2+5x1x2+2\displaystyle \lim_{x \to \infty} \frac{3x^2 + 5x - 1}{x^2 + 2}.

📐 Expand: full pencil-and-paper solution
Step 1 — Identify the highest power. Highest power of xx in either polynomial is x2x^2.
Step 2 — Divide top and bottom by x2x^2.
3x2+5x1x2+2=3+5x1x21+2x2.\frac{3x^2 + 5x - 1}{x^2 + 2} = \frac{3 + \dfrac{5}{x} - \dfrac{1}{x^2}} {1 + \dfrac{2}{x^2}}.
Step 3 — Send each piece to its limit. 5/x05/x \to 0, 1/x201/x^2 \to 0, 2/x202/x^2 \to 0. The numerator approaches 33; the denominator approaches 11.
Step 4 — Apply the quotient rule for limits. Because both pieces have finite limits and the denominator limit is nonzero,
limx3x2+5x1x2+2=31=3.\lim_{x\to\infty} \frac{3x^2+5x-1}{x^2+2} = \frac{3}{1} = 3.
Step 5 — Sanity-check numerically. f(1000)3.005f(1000) \approx 3.005 — very close to 3, as expected. The Python code below reproduces this table and shows the error shrinks like 5/x5/x.

Essential Limits You Must Know

A small vocabulary of standard end-behaviors makes every future problem easier.

ExpressionLimit as x → ∞Why
1 / x^p (p > 0)0Polynomial in the denominator outgrows 1.
e^(-x)0Exponential decay beats every polynomial.
arctan(x)π/2 ≈ 1.5708Slope of arctan → 0; range bounded above by π/2.
1 / ln(x)0ln(x) → ∞, just slowly.
sin(x)does not existOscillates in [−1, 1]; never settles.
(1 + 1/x)^xe ≈ 2.71828Euler's limit — the compound-interest fingerprint.
Growth hierarchy at infinity — from slowest to fastest: lnx    xp    ex    x!\ln x \;\ll\; x^p \;\ll\; e^x \;\ll\; x!. When a quotient mixes these, the faster grower decides the fate.

Python: Building Intuition Numerically

Reading a proof is one thing; watching the limit form, row by row, is another. The script below probes our example function at a geometric ladder of x-values and tabulates f(x)L|f(x) - L|. Click any line to see what Python is doing at that moment.

Numerical probe — (3x² + 5x − 1) / (x² + 2) → 3
🐍limit_at_infinity.py
1import math

Python's standard math module. We use it only for math.isclose() at the end — a safe float comparison that tolerates tiny rounding error. We could have used '==' but floats are never exactly equal after arithmetic; isclose() answers 'are these the same number, within rounding?'

EXECUTION STATE
📚 math = Standard library module. Provides isclose(a, b), sqrt, log, exp, trig, constants like math.pi. No arrays — just scalars.
why not == = 0.1 + 0.2 == 0.3 returns False in Python (binary floats cannot represent 0.1 exactly). isclose() bakes in a tiny tolerance.
3def f(x: float) → float

Defines the function we want to analyze. Take a real number x, return the rational expression (3x² + 5x − 1) / (x² + 2). This is our 'test subject' — we will feed it larger and larger x values and watch where the output goes.

EXECUTION STATE
⬇ input: x (float) = The point at which we evaluate f. For the limit at infinity, we will call f(x) for x = 1, 10, 100, 1000, ...
→ type hints : float → float = Annotations for humans & linters. Python does not enforce them; they document that f expects one real number and returns one real number.
⬆ returns = A single float — the value of the rational function at x. As x grows, this value should approach 3.
5return (3 * x**2 + 5 * x - 1) / (x**2 + 2)

Computes one value of the rational function. The ** operator is exponentiation in Python (NOT bitwise XOR — that is ^). Order of operations: ** first, then * and /, then + and −. So (3 * x**2) means 3·(x²), never (3·x)².

EXECUTION STATE
x ** 2 = x raised to the power 2. Example: if x = 10, then x**2 = 100. If x = 1000, x**2 = 1,000,000.
numerator: 3*x**2 + 5*x - 1 = At x=10: 3·100 + 50 − 1 = 349 At x=100: 3·10000 + 500 − 1 = 30,499 At x=1000: 3,004,999
denominator: x**2 + 2 = At x=10: 102 At x=100: 10,002 At x=1000: 1,000,002
⬆ return = f(10) = 349 / 102 ≈ 3.4215686 f(100) = 30499 / 10002 ≈ 3.0492901 f(1000)= 3004999 / 1000002 ≈ 3.0049930
7def rewrite_divided_by_leading(x: float) → float

Same function rewritten with every term divided by x² (the highest power of x appearing anywhere). This is the trick that exposes the limit: terms like 5/x and 1/x² shrink to 0 as x → ∞, so the formula collapses to 3/1 = 3.

EXECUTION STATE
⬇ input: x (float) = Same x as before — we evaluate the same function, just in a different algebraic form.
the idea = (3x² + 5x − 1) / (x² + 2) × (1/x²)/(1/x²) = (3 + 5/x − 1/x²) / (1 + 2/x²)
⬆ returns = Exactly the same number as f(x) for every x ≠ 0, but now we can SEE the limit: as x→∞, 5/x → 0, 1/x² → 0, 2/x² → 0, leaving 3/1.
9numerator = 3 + 5 / x - 1 / x**2

Computes the rewritten numerator. Notice how small the last two terms become for large x — that is the whole story of the limit compressed into one line.

EXECUTION STATE
5 / x = x=10 → 0.5 x=100 → 0.05 x=1000 → 0.005 x=∞ → 0
1 / x**2 = x=10 → 0.01 x=100 → 0.0001 x=1000 → 1e-6 x=∞ → 0
numerator = x=10 → 3 + 0.5 − 0.01 = 3.49 x=100 → 3 + 0.05 − 0.0001 = 3.0499 x=1000→ 3 + 0.005 − 1e-6 ≈ 3.005
10denominator = 1 + 2 / x**2

Rewritten denominator. The '2' in the original (x² + 2) became 2/x² after dividing by x². It collapses to 0, leaving just 1.

EXECUTION STATE
2 / x**2 = x=10 → 0.02 x=100 → 0.0002 x=1000 → 2e-6 x=∞ → 0
denominator = x=10 → 1.02 x=100 → 1.0002 x=1000→ 1.000002 x=∞ → 1
11return numerator / denominator

Divides the two rewritten pieces. For large x the result is (≈3) / (≈1) = 3 — the limit.

EXECUTION STATE
/ (division) = Floating-point division in Python 3. 349/102 is 3.4215686..., NOT 3 (integer-style truncation would need // instead).
⬆ return = Identical value to f(x). Different path, same destination — algebraic identity.
14xs = [1, 10, 100, 1_000, 10_000, 100_000]

Our probe points. We deliberately pick a geometric sequence (each x ten times larger than the last) so we can watch the error shrink by about a factor of 10 each row — a visual fingerprint of a 1/x convergence rate.

EXECUTION STATE
xs (list of ints) = [1, 10, 100, 1000, 10000, 100000]
1_000 notation = Python 3.6+ allows underscores as digit separators. 1_000 is identical to 1000 — a readability trick, not a new number type.
why geometric spacing? = If |f(x)−L| behaves like C/x, then multiplying x by 10 divides the error by 10. Constant spacing (1,2,3,...) would hide this pattern.
15L = 3.0 # conjectured limit

We declare what we believe the limit is. The rest of the code is the experiment that either supports the conjecture or refutes it. (It will support it — by the divided-by-leading-power argument above.)

EXECUTION STATE
L = 3.0 — the horizontal asymptote. Same as the ratio of leading coefficients: 3/1 = 3.
17print(header) # format the table header

Prints a right-aligned column header using Python's format specifier. '>7' means 'right-align in a field 7 characters wide'. The table will line up neatly because every number uses the same field width.

EXECUTION STATE
f-string prefix: f'...' = Formatted string literal. Everything inside { } is evaluated and inserted. Introduced in Python 3.6.
{'x':>7} = Insert the string 'x', right-aligned in a 7-char field → ' x'. Same width as later rows → columns align.
18print("-" * 42)

String multiplication: '-' * 42 produces a 42-dash separator. Pure Python convenience — no math behind it.

EXECUTION STATE
* on a string = Repeats the string N times. '-' * 42 = '------------------------------------------'
19for x in xs: — probe each test point

Iterate the list xs and compute f(x) and the distance to L for each. This is the heart of the experiment: we are literally watching the limit form.

LOOP TRACE · 6 iterations
x = 1
f(x) = 2.33333333
|f(x) − 3| = 6.67e-01 — still far from 3
x = 10
f(x) = 3.42156863
|f(x) − 3| = 4.22e-01 — overshoots 3, then drifts back
x = 100
f(x) = 3.04929014
|f(x) − 3| = 4.93e-02 — error shrinks ~10×
x = 1,000
f(x) = 3.00499299
|f(x) − 3| = 4.99e-03 — shrinks another ~10×
x = 10,000
f(x) = 3.00049993
|f(x) − 3| = 5.00e-04
x = 100,000
f(x) = 3.00004999
|f(x) − 3| = 5.00e-05 — each ×10 in x divides error by 10
20y = f(x)

Calls f() with the current x and stores the result in y. Pure variable assignment — no new math.

EXECUTION STATE
y = Current function value. Updated every loop iteration. Example after x=100: y = 3.04929014.
21gap = abs(y - L)

The DISTANCE from the function value to our conjectured limit. This is ε in the formal definition — the quantity that MUST shrink to 0 for the limit to exist.

EXECUTION STATE
📚 abs() = Built-in: absolute value. abs(-3) = 3, abs(3) = 3. Works for int, float, and complex (returns magnitude).
⬇ arg: y - L = Signed error. Can be negative when f dips below L. abs() strips the sign because we only care about magnitude.
⬆ gap = The number that must → 0. If it does → limit is confirmed. If it stays bounded away from 0 → limit does NOT exist.
22print(f"{x:>7} | {y:>14.8f} | {gap:>14.2e}")

Prints one row of the table with three formatted columns. The format specifier after ':' controls how each value is rendered.

EXECUTION STATE
{y:>14.8f} = Right-align (>), field width 14, 8 digits after the decimal, fixed-point (f). So 3.04929014 is shown as ' 3.04929014'.
{gap:>14.2e} = Scientific notation (e) with 2 digits after the decimal. 0.0493 → ' 4.93e-02'. Scientific notation makes the 10× drops obvious.
output row example = 100 | 3.04929014 | 4.93e-02
25same = all(math.isclose(f(x), rewrite_divided_by_leading(x)) for x in xs)

Cross-check: for every probe point, verify that the original form and the rewritten form give the same number. This is a sanity test that our algebraic manipulation was correct — a crucial habit in math-meets-code.

EXECUTION STATE
📚 all(iterable) = Built-in: returns True if EVERY element of the iterable is truthy. all([True, True]) → True. all([True, False]) → False. all([]) → True (vacuous).
📚 math.isclose(a, b) = Robust float equality. Returns True if |a − b| ≤ max(rel_tol·max(|a|,|b|), abs_tol). Defaults: rel_tol=1e-9, abs_tol=0. Equivalent to 'these floats agree to 9 significant digits'.
generator expression = (math.isclose(...) for x in xs) — evaluated lazily, one element at a time. Same shape as a list comp but without building the intermediate list.
⬆ same = True — the two algebraic forms are identical for every x. This is the algebraic identity's fingerprint in code.
26print(f"\nBoth forms agree everywhere? {same}")

Prints the sanity-check result with a leading blank line (\n) for visual separation from the table above.

EXECUTION STATE
\n = Escape sequence for a newline character. Inserted here to separate the table from the final verdict line.
printed output = Both forms agree everywhere? True
9 lines without explanation
1import math
2
3def f(x: float) -> float:
4    """The function whose limit at infinity we study."""
5    return (3 * x**2 + 5 * x - 1) / (x**2 + 2)
6
7def rewrite_divided_by_leading(x: float) -> float:
8    """Divide numerator and denominator by x^2 — the *highest power*."""
9    numerator   = 3 + 5 / x - 1 / x**2
10    denominator = 1 + 2 / x**2
11    return numerator / denominator
12
13# Probe ever-larger x to see where f(x) is heading
14xs = [1, 10, 100, 1_000, 10_000, 100_000]
15L  = 3.0   # conjectured limit
16
17print(f"{'x':>7} | {'f(x)':>14} | {'|f(x) - L|':>14}")
18print("-" * 42)
19for x in xs:
20    y = f(x)
21    gap = abs(y - L)
22    print(f"{x:>7} | {y:>14.8f} | {gap:>14.2e}")
23
24# Verify the algebraic form gives the same numbers
25same = all(math.isclose(f(x), rewrite_divided_by_leading(x)) for x in xs)
26print(f"\nBoth forms agree everywhere? {same}")

Two observations make the limit feel inevitable:

  1. The rewrite column collapses to 3/13/1 as the 1/x1/x and 1/x21/x^2 terms die.
  2. The error column shrinks by a factor of 10 every time xx grows by a factor of 10 — a visual fingerprint of f(x)L5/x|f(x)-L| \sim 5/x.

PyTorch: The Same Idea, on Tensors

Now we rebuild the experiment with tensors — so the entire ladder is evaluated in a single vectorized call — and use autograd to verify the convergence rate without any hand-computed derivative.

PyTorch — vectorized probe + autograd rate check
🐍limit_torch.py
1import torch

Loads PyTorch. PyTorch gives us two things that pure-Python math cannot: (1) torch.Tensor — a vectorized array that runs the same formula on many numbers in parallel, and (2) autograd — automatic differentiation for verifying how fast the limit converges.

EXECUTION STATE
📚 torch = Tensor library + autograd engine + GPU runtime. Tensors are like NumPy arrays but can live on a GPU and track gradients.
why use it here? = The limit question is not ML — but PyTorch's tensor ops let us evaluate f on a whole array at once, and .backward() lets us check the decay rate symbolically, with zero hand-calculus.
3def f_tensor(x: torch.Tensor) → torch.Tensor

Exactly the same formula as before, but when x is a tensor every operation is BROADCAST — applied element-wise to every entry of x in parallel. No Python-level loop required.

EXECUTION STATE
⬇ input: x (Tensor) = A 1-D tensor of probe points. Example: tensor([1., 10., 100., 1e3, 1e4, 1e5]). Any shape works — the formula broadcasts over all of it.
broadcasting = When we write '3 * x**2' and x has 6 elements, PyTorch computes 6 values in one vectorized call. Python loop ≈ 6 µs; tensor op ≈ 600 ns.
⬆ returns = Tensor of the same shape as x, containing f(x_i) for each entry.
5return (3 * x**2 + 5 * x - 1) / (x**2 + 2)

Element-wise arithmetic. Every operator (*, **, +, -, /) is overloaded on torch.Tensor to apply piecewise and return a new tensor. No mutation.

EXECUTION STATE
x ** 2 = Element-wise square. tensor([1, 10, 100]) ** 2 → tensor([1., 100., 10000.])
⬆ return = tensor([2.3333, 3.4216, 3.0493, 3.0050, 3.0005, 3.0000]) — each entry is f(x_i).
8x = torch.logspace(start=0, end=5, steps=6)

Creates a tensor of 6 points spaced LOGARITHMICALLY between 10⁰ and 10⁵. The output is exactly [1, 10, 100, 1000, 10000, 100000] — a geometric ladder, hand-made for seeing the 1/x decay.

EXECUTION STATE
📚 torch.logspace(start, end, steps, base=10.0) = Returns steps values between base**start and base**end, spaced evenly in LOG domain. Example: torch.logspace(0, 3, 4) → [1, 10, 100, 1000].
⬇ arg: start=0 = Exponent of the first point. 10**0 = 1.
⬇ arg: end=5 = Exponent of the last point. 10**5 = 100,000.
⬇ arg: steps=6 = How many points to generate, including both endpoints. 6 points → exponents 0,1,2,3,4,5 → [1,10,100,1e3,1e4,1e5].
⬆ x = tensor([1.0000e+00, 1.0000e+01, 1.0000e+02, 1.0000e+03, 1.0000e+04, 1.0000e+05])
10y = f_tensor(x)

Calls f_tensor on the whole 6-element ladder. One line, six evaluations — this is the vectorized punchline.

EXECUTION STATE
y = tensor([2.3333333, 3.4215686, 3.0492902, 3.0049930, 3.0004999, 3.0000500])
11gap = (y - 3.0).abs()

Subtracting a Python scalar from a tensor broadcasts — 3.0 is applied to every entry. .abs() is the element-wise absolute value. 'gap' is now the ε of the formal definition, one per probe point.

EXECUTION STATE
📚 tensor.abs() = Element-wise absolute value. (-3.2).abs() = 3.2. Also spelled torch.abs(tensor) — identical.
⬇ arg: y - 3.0 = Signed error tensor: tensor([-0.6667, 0.4216, 0.0493, 0.0050, 0.0005, 0.00005]).
⬆ gap = tensor([6.67e-01, 4.22e-01, 4.93e-02, 4.99e-03, 5.00e-04, 5.00e-05]) — shrinks by ~10× per step, confirming 1/x rate.
13for xi, yi, gi in zip(x.tolist(), y.tolist(), gap.tolist()):

Move the three tensors back to plain Python lists (for nice printing) and iterate their entries together with zip().

LOOP TRACE · 6 iterations
xi=1, yi=2.33e0, gi=6.67e-1
row = x= 1 f(x)=2.33333333 gap=6.67e-01
xi=10, yi=3.42e0, gi=4.22e-1
row = x= 10 f(x)=3.42156863 gap=4.22e-01
xi=100, yi=3.05e0, gi=4.93e-2
row = x= 100 f(x)=3.04929014 gap=4.93e-02
xi=1e3, yi=3.005e0, gi=4.99e-3
row = x= 1000 f(x)=3.00499299 gap=4.99e-03
xi=1e4, yi=3.0005e0, gi=5.00e-4
row = x= 10000 f(x)=3.00049993 gap=5.00e-04
xi=1e5, yi=3.00005e0, gi=5.00e-5
row = x= 100000 f(x)=3.00004999 gap=5.00e-05
14print(f"x={xi:>10.0f} f(x)={yi:.8f} gap={gi:.2e}")

One formatted line per row. :>10.0f right-aligns in 10 chars with 0 decimals (an integer format for xi). .8f gives 8 decimals; .2e gives scientific notation with 2 decimals.

EXECUTION STATE
{xi:>10.0f} = Right-align (>), width 10, 0 decimals, fixed (f). Prints 100000 as ' 100000'.
20x_big = torch.tensor(1_000.0, requires_grad=True)

Creates a scalar tensor at x = 1000 that TRACKS GRADIENTS. 'requires_grad=True' tells autograd: 'remember every operation you perform on me; I want the derivative later.' This is the magic that lets us check 'how fast does the error decay?' without any hand-calculus.

EXECUTION STATE
📚 torch.tensor(data, requires_grad=False) = Factory: builds a new tensor from a Python number, list, or ndarray.
⬇ arg: data = 1_000.0 = The numerical value. Must be a float (or cast-able) because gradients require real-valued differentiation.
⬇ arg: requires_grad = True = Turns on the autograd tape. Every tensor derived from x_big now carries a graph pointing back to x_big so .backward() can compute ∂/∂x_big.
⬆ x_big = tensor(1000., requires_grad=True) — a 0-dimensional tensor (a scalar).
21y_big = f_tensor(x_big)

Evaluates f at the leaf tensor. Because x_big tracks gradients, y_big inherits a full computation graph: every +, *, **, / we used is recorded.

EXECUTION STATE
y_big = tensor(3.00499..., grad_fn=<DivBackward0>)
grad_fn = Pointer to the last operation that produced this tensor (division). Walking backward through grad_fn is how autograd computes derivatives.
22y_big.backward()

Triggers reverse-mode autodiff. PyTorch walks the grad_fn graph from y_big back to x_big, multiplying local derivatives (the chain rule), and deposits dY/dx into x_big.grad.

EXECUTION STATE
📚 Tensor.backward() = For a SCALAR output, .backward() computes the gradient of that scalar w.r.t. every leaf tensor with requires_grad=True. For non-scalars, you must pass a 'grad_outputs' tensor.
what gets computed = ∂y_big / ∂x_big, evaluated at x_big = 1000. Analytically: f′(x) = (5x² + 14x + 10) / (x² + 2)². At x=1000 ≈ 5.00e-6.
side effect = x_big.grad ← tensor(5.000e-6) — the derivative is WRITTEN into the leaf, not returned.
23print(f"\nAt x=1000: f'(x) ≈ {x_big.grad.item():.2e} (theory: 5/1e6 = 5e-6)")

Fetches the gradient and prints it. .item() converts a 0-d tensor to a plain Python float so we can format it. The printed value matches the hand-calculated leading-order 5/x² exactly — a cross-check that the limit converges at rate ~1/x as the error column in the table also showed.

EXECUTION STATE
📚 Tensor.item() = Scalar → Python float. ONLY works on 0-d tensors (size 1, no dimensions). Trying .item() on a multi-entry tensor raises an error.
printed output = At x=1000: f'(x) ≈ 5.00e-06 (theory: 5/1e6 = 5e-6) ✓
why we care = f′(x) measures how fast f is changing at x. If f′(x) → 0 quickly (here ~1/x²), f is flattening → the horizontal asymptote is real and stable.
10 lines without explanation
1import torch
2
3def f_tensor(x: torch.Tensor) -> torch.Tensor:
4    """Vectorized rational function — evaluates on a whole tensor at once."""
5    return (3 * x**2 + 5 * x - 1) / (x**2 + 2)
6
7# Build a geometric ladder of probe points on the GPU/CPU
8x = torch.logspace(start=0, end=5, steps=6)   # [1, 10, 100, 1e3, 1e4, 1e5]
9
10y   = f_tensor(x)
11gap = (y - 3.0).abs()
12
13for xi, yi, gi in zip(x.tolist(), y.tolist(), gap.tolist()):
14    print(f"x={xi:>10.0f}  f(x)={yi:.8f}  gap={gi:.2e}")
15
16# Autograd: does |f(x) - 3| really shrink like 5/x?
17# d/dx [ (3x^2+5x-1)/(x^2+2) ] = (5x^2 + 14x + 10) / (x^2+2)^2
18# For large x, derivative ~ 5/x^2  → error decays like 1/x ✓
19x_big = torch.tensor(1_000.0, requires_grad=True)
20y_big = f_tensor(x_big)
21y_big.backward()
22print(f"\nAt x=1000:  f'(x) ≈ {x_big.grad.item():.2e}  (theory: 5/1e6 = 5e-6)")

The same table falls out, but two things are different:

  • Vectorized evaluation — one tensor call, six answers. This is exactly how deep-learning libraries compute losses over batches.
  • Automatic differentiation — PyTorch produces f(1000)5×106f'(1000) \approx 5 \times 10^{-6} without us writing the derivative. Since f(x)0f'(x) \to 0, the function is flattening — the horizontal asymptote is not a numerical illusion, it is structural.

Why This Matters — Real-World Stories

🪂 Terminal velocity

The skydiver's speed v(t)=v(1ekt)v(t) = v_\infty(1 - e^{-kt}) has horizontal asymptote vv_\infty — the terminal velocity. Every safety chart is an ε-N argument in disguise.

🧬 Logistic growth

Bacteria, tumors, social-network adoption: P(t)=K1+AertP(t) = \dfrac{K}{1 + A e^{-rt}} has horizontal asymptote KK (the carrying capacity). Long-term forecasts rest on this limit.

📡 Sampling & convergence

“The sample mean converges to the expected value as nn \to \infty” is the law of large numbers — a limit at infinity in the exact sense of this section.

🤖 Learning-rate schedules

In deep learning, cosine and inverse-square-root schedules drive the learning rate to 00 as training steps \to \infty. The asymptote makes the optimizer stop wobbling near the minimum.


Common Pitfalls

  • Plugging in ∞ directly. \infty is not a number. “/\infty/\infty” is an indeterminate form — the divide-by-highest-power trick resolves it.
  • Only checking one direction. x+x \to +\infty and xx \to -\infty can give different answers. arctan\arctan is the canonical example.
  • Assuming smoothness means settling. sin(x)\sin(x) is smooth and bounded but has no limit at infinity — it never commits.
  • Confusing numerical proximity with proof. A Python table can suggest a limit but cannot prove one. The ε–N argument is what actually discharges the claim.

Summary

A limit at infinity is a contract between a tolerance ε\varepsilon and a cut-off NN: for any challenge ε\varepsilon the function must promise to live inside an ε\varepsilon-band around LL for all x>Nx > N. When such a contract exists the horizontal line y=Ly = L is the function's horizontal asymptote.

For rational functions, the degree race of numerator and denominator settles the answer immediately. For other functions, we lean on the growth hierarchy or rewrite the expression until every vanishing piece is made visible. Python lets us watch the convergence row by row; PyTorch lets us differentiate through it to confirm the rate. In every application — terminal velocity, logistic growth, law of large numbers, learning-rate decay — the same idea shows up: a process that settles.

Loading comments...