Chapter 19
26 min read
Section 164 of 353

Line Integrals

Vector Calculus

Learning Objectives

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

  1. Understand the concept of a line integral as integrating a function along a curve, rather than over a straight interval on the x-axis
  2. Distinguish between scalar and vector line integrals (Cfds\int_C f\, ds vs CFdr\int_C \mathbf{F} \cdot d\mathbf{r}) and pick the right one for the problem
  3. Derive the arc-length element ds=r(t)dtds = |\mathbf{r}'(t)|\, dt from first principles
  4. Evaluate line integrals by parametrization and reduce a curve integral to a familiar one-variable integral in tt
  5. Compute work done by a force field along a curved path
  6. Implement line integrals in plain Python and then in PyTorch with automatic differentiation
Why this matters: A line integral generalizes the integralabf(x)dx\int_a^b f(x)\, dx from straight intervals to arbitrary curved paths. Once you have it, you can compute mass of a non-uniform wire, work done by a varying force along a winding road, circulation of fluid around a closed loop, the EMF in a circuit by Maxwell's equations, and the cost accumulated by an optimizer as it walks through a loss landscape.

The Big Picture

Standard one-variable integration abf(x)dx\int_a^b f(x)\, dx sweeps across a straight interval on the x-axis. A line integral sweeps across an arbitrary curve CC in 2D or 3D space. Everything else stays the same: we slice the path into infinitesimal pieces, multiply each piece by the value of the function there, and sum.

Two flavors of line integral:
  1. Scalar line integral Cfds\int_C f\, ds: weighs a scalar function f(x,y)f(x, y) by the arc-length element dsds. Answer to "how much of f piles up along the curve?"
  2. Vector line integral CFdr\int_C \mathbf{F} \cdot d\mathbf{r}: projects a vector field F\mathbf{F} onto the direction of motion. Answer to "how much of F is pushing me along the curve?"

The scalar form does not care which way you walk the curve; the vector form does — reverse direction and the sign flips. That difference will turn out to be the foundation of conservative fields, Green's theorem, and Stokes' theorem in the sections to come.


Where ds Comes From (Intuition)

Before any formula, picture this. You parametrize a curve with r(t)=(x(t),y(t))\mathbf{r}(t) = (x(t), y(t)). The parameter tt is like time. As tt ticks forward by a tiny amount dtdt, the point on the curve moves by a tiny displacement vector dr=r(t)dtd\mathbf{r} = \mathbf{r}'(t)\, dt.

The length of that tiny displacement is the arc-length element: ds=dr=r(t)dtds = |d\mathbf{r}| = |\mathbf{r}'(t)|\, dt. This is just the Pythagorean theorem in infinitesimal form —ds=(dx)2+(dy)2=(x(t))2+(y(t))2dtds = \sqrt{(dx)^2 + (dy)^2} = \sqrt{(x'(t))^2 + (y'(t))^2}\, dt.

One slogan: r(t)|\mathbf{r}'(t)| is the conversion factor between parameter time and arc length. If the parametrization is twice as fast at some point (e.g. r(t)=2|\mathbf{r}'(t)| = 2), then one unit of dtdt sweeps out two units of arc length dsds there.

That is the entire trick. A scalar line integral measures fdsf \cdot ds — a value times a length. A vector line integral measures Fdr\mathbf{F} \cdot d\mathbf{r} — a force dotted with a displacement. Everything that follows is bookkeeping.


Historical Context

Line integrals were invented to answer concrete physical questions in the 18th and 19th centuries: Lagrange and Laplace used path integrals to study gravitational potentials and the motion of planets; Cauchy formalized them in the complex plane, leading to the powerful Cauchy integral theorem; George Green connected line integrals to double integrals through Green's Theorem; and Maxwell built electromagnetism on top of them — the EMF around a loop and the current through a surface are both line/surface integrals. The unifying problem was always the same: a force that varies along a curved path makes the schoolbook formula work = force times distance meaningless, and line integrals fix it.


Scalar Line Integrals

Definition and Notation

Given a scalar function f(x,y)f(x, y) and a smooth curve CC, the scalar line integral of ff along CC is the limit of Riemann sums: Cfds=limni=1nf(xi,yi)Δsi\int_C f\, ds = \lim_{n \to \infty} \sum_{i=1}^{n} f(x_i^*, y_i^*)\, \Delta s_i, where the curve is sliced into nn small arcs of length Δsi\Delta s_i and (xi,yi)(x_i^*, y_i^*) is any sample point on the ii-th arc.

Physical reading: if f(x,y)=ρ(x,y)f(x, y) = \rho(x, y) is the linear mass density (mass per unit length) of a wire shaped like CC, then Cρds\int_C \rho\, ds is the total mass of the wire.

Evaluation Using Parametrization

To actually compute, parametrize r(t)=(x(t),y(t))\mathbf{r}(t) = (x(t), y(t)) for atba \le t \le b. Substituting ds=r(t)dtds = |\mathbf{r}'(t)|\, dt: Cfds=abf(x(t),y(t))r(t)dt\int_C f\, ds = \int_a^b f(x(t), y(t))\, |\mathbf{r}'(t)|\, dt. The line integral on the left (geometric, hard to compute directly) becomes a plain single-variable integral on the right (mechanical).

The Arc Length Element ds

Spelled out: ds=(dxdt)2+(dydt)2dtds = \sqrt{\left(\tfrac{dx}{dt}\right)^2 + \left(\tfrac{dy}{dt}\right)^2}\, dt. In 3D add a (dz/dt)2(dz/dt)^2 term. When f=1f = 1, the scalar line integral collapses to the arc length: Length(C)=C1ds=abr(t)dt\text{Length}(C) = \int_C 1\, ds = \int_a^b |\mathbf{r}'(t)|\, dt.

Reparametrization invariance: if you reparametrize CC with a different (smooth, monotonic) function t(u)t(u), the answer does not change. The r(t)|\mathbf{r}'(t)| factor automatically corrects for how fast the new parameter sweeps the curve.

Worked Example (Step by Step)

We will compute both a scalar and a vector line integral on the same curve, so you can see exactly what is shared and what is different. The curve is the parabola y=x2y = x^2 from (0,0)(0, 0) to (1,1)(1, 1).

Setup we will use throughout:
  • Parametrization r(t)=(t, t2)\mathbf{r}(t) = (t,\ t^2) for t[0,1]t \in [0, 1]
  • Velocity r(t)=(1, 2t)\mathbf{r}'(t) = (1,\ 2t)
  • Speed r(t)=1+4t2|\mathbf{r}'(t)| = \sqrt{1 + 4t^2}
Part 1 — Mass of a wire (scalar line integral). Click to expand.

Imagine the parabola is a thin wire with linear mass density ρ(x,y)=1+x\rho(x, y) = 1 + x (kilograms per meter). Heavier at the right end, lighter at the origin. Total mass is M=CρdsM = \int_C \rho\, ds.

Step 1. Substitute the parametrization: ρ(r(t))=ρ(t,t2)=1+t\rho(\mathbf{r}(t)) = \rho(t, t^2) = 1 + t.

Step 2. Substitute ds=r(t)dt=1+4t2dtds = |\mathbf{r}'(t)|\, dt = \sqrt{1 + 4t^2}\, dt.

Step 3. The line integral becomes the ordinary integral M=01(1+t)1+4t2dtM = \int_0^1 (1 + t)\, \sqrt{1 + 4t^2}\, dt.

Step 4. Split into two pieces: M=011+4t2dt+01t1+4t2dtM = \int_0^1 \sqrt{1 + 4t^2}\, dt + \int_0^1 t\, \sqrt{1 + 4t^2}\, dt.

Step 5. The first piece is a standard arc-length-style integral. Using u=2tu = 2t: 011+4t2dt=12[t1+4t2+12ln(2t+1+4t2)]01\int_0^1 \sqrt{1 + 4t^2}\, dt = \tfrac{1}{2}\left[t\sqrt{1 + 4t^2} + \tfrac{1}{2}\ln(2t + \sqrt{1 + 4t^2})\right]_0^1, which evaluates numerically to 1.4789\approx 1.4789.

Step 6. The second piece is trivial with u=1+4t2u = 1 + 4t^2, du=8tdtdu = 8t\, dt: 01t1+4t2dt=112[(1+4t2)3/2]01=112(551)0.3089\int_0^1 t \sqrt{1 + 4t^2}\, dt = \tfrac{1}{12}\left[(1 + 4t^2)^{3/2}\right]_0^1 = \tfrac{1}{12}(5\sqrt{5} - 1) \approx 0.3089.

Step 7. Add them: M1.4789+0.3089=1.7878M \approx 1.4789 + 0.3089 = 1.7878 kilograms. We will confirm this numerically with the Python implementation below.

Part 2 — Work done by a force field (vector line integral). Click to expand.

Now keep the same curve but switch from a scalar density to a vector force field F(x,y)=(2xy, x2)\mathbf{F}(x, y) = (2xy,\ x^2). We want the work W=CFdrW = \int_C \mathbf{F} \cdot d\mathbf{r}.

Step 1. Evaluate F\mathbf{F} on the curve: F(r(t))=(2tt2, t2)=(2t3, t2)\mathbf{F}(\mathbf{r}(t)) = (2 \cdot t \cdot t^2,\ t^2) = (2t^3,\ t^2).

Step 2. Take the dot product with r(t)=(1,2t)\mathbf{r}'(t) = (1, 2t): F(r(t))r(t)=2t31+t22t=4t3\mathbf{F}(\mathbf{r}(t)) \cdot \mathbf{r}'(t) = 2t^3 \cdot 1 + t^2 \cdot 2t = 4t^3.

Step 3. The line integral becomes W=014t3dt=t401=1W = \int_0^1 4t^3\, dt = t^4 \Big|_0^1 = 1.

Step 4 (sanity check via potential). Notice that F=ϕ\mathbf{F} = \nabla \phi with ϕ(x,y)=x2y\phi(x, y) = x^2 y. For any conservative field, the work equals the change in potential: W=ϕ(1,1)ϕ(0,0)=10=1W = \phi(1, 1) - \phi(0, 0) = 1 - 0 = 1. Same answer, confirmed. This is a preview of the Fundamental Theorem for Line Integrals in the next section.

Notice what was shared and what was different. Both integrals used the same r(t)\mathbf{r}(t), the same r(t)\mathbf{r}'(t), and the same bounds. The scalar version multiplied by r(t)|\mathbf{r}'(t)|; the vector version dotted with r(t)\mathbf{r}'(t). That is the only mechanical difference.

Interactive: Scalar Line Integral

Explore how scalar line integrals work by integrating different functions along various curves. Watch how the Riemann sum approximation converges as you increase the number of segments. The colors indicate the value of the function at each point along the curve.

Loading 3D visualization...
Things to try:
  • Switch to arc-length mode with the helix to see 3D integration
  • Compare the circle and the line — for f(x,y)=x2+y2f(x, y) = x^2 + y^2, which gives a larger integral?
  • Crank up the number of segments and watch the Riemann sum converge

Interactive: Parametric Curves and ds

Understanding the relationship between the parameter tt and the arc-length element dsds is crucial. Drag the slider to move along the curve and watch how ds=r(t)dtds = |\mathbf{r}'(t)|\, dt turns parameter changes into actual distances.

Loading parametric curve demo...

Vector Line Integrals

Work and the Line Integral of a Vector Field

When a force F\mathbf{F} moves an object along a path CC, the work done is W=CFdrW = \int_C \mathbf{F} \cdot d\mathbf{r}. This is the vector line integral (or work integral). Unlike its scalar cousin, it depends on the direction of traversal: walk the curve backwards and the answer flips sign.

The Dot Product Form

With parametrization r(t)\mathbf{r}(t) for atba \le t \le b: CFdr=abF(r(t))r(t)dt\int_C \mathbf{F} \cdot d\mathbf{r} = \int_a^b \mathbf{F}(\mathbf{r}(t)) \cdot \mathbf{r}'(t)\, dt. Writing F=(P,Q)\mathbf{F} = (P, Q), this is also CPdx+Qdy\int_C P\, dx + Q\, dy, where dx=x(t)dtdx = x'(t)\, dt and dy=y(t)dtdy = y'(t)\, dt.

Physical reading: the dot product Fdr\mathbf{F} \cdot d\mathbf{r} projects the force onto the direction of motion. Force aligned with the motion contributes positive work; force opposed to the motion contributes negative work; force perpendicular to the motion contributes nothing.

Interactive: Work Done by a Force Field

Explore how different vector fields do work along various paths. Green segments show positive work (force helping motion), red segments show negative work (force opposing motion). Notice how the total work depends on both the field and the path.

Loading work integral visualization...
Key things to observe:
  • For the radial field F=(x,y)\mathbf{F} = (x, y), work between two points is the same for every path you draw — it is conservative
  • For the rotational field F=(y,x)\mathbf{F} = (-y, x), work around a closed loop is nonzero and equals twice the enclosed area
  • For gravity F=(0,1)\mathbf{F} = (0, -1), only vertical motion does work; horizontal motion does none

Properties of Line Integrals

PropertyScalar line integralVector line integral
Direction dependenceIndependent of directionSign flips if direction reverses
Additivity over pathsIntegral on C1C2C_1 \cup C_2 = sum of piecesSame: integral on C1C2C_1 \cup C_2 = sum of pieces
ReparametrizationInvariant under any smooth reparamInvariant if orientation is preserved
Closed-curve resultCan be any valueZero for conservative fields
Direction matters for vector integrals. Let C-C denote CC traversed in the reverse direction. Then:
  • Scalar: Cfds=Cfds\int_{-C} f\, ds = \int_C f\, ds
  • Vector: CFdr=CFdr\int_{-C} \mathbf{F} \cdot d\mathbf{r} = -\int_C \mathbf{F} \cdot d\mathbf{r}

More Worked Examples

Example A — Scalar line integral on a straight segment. Click to expand.

Compute C(x2+y)ds\int_C (x^2 + y)\, ds where CC is the line segment from (0,0)(0, 0) to (3,4)(3, 4).

Parametrize: r(t)=(3t,4t)\mathbf{r}(t) = (3t, 4t) for 0t10 \le t \le 1. Then r(t)=(3,4)\mathbf{r}'(t) = (3, 4) and r(t)=9+16=5|\mathbf{r}'(t)| = \sqrt{9 + 16} = 5.

Substitute: C(x2+y)ds=01((3t)2+4t)5dt=501(9t2+4t)dt\int_C (x^2 + y)\, ds = \int_0^1 ((3t)^2 + 4t) \cdot 5\, dt = 5\int_0^1 (9t^2 + 4t)\, dt.

Evaluate: =5[3t3+2t2]01=5(3+2)=25= 5 [3t^3 + 2t^2]_0^1 = 5(3 + 2) = 25.

Example B — Vector line integral on a parabola. Click to expand.

Compute CFdr\int_C \mathbf{F} \cdot d\mathbf{r} where F=(2xy,x2)\mathbf{F} = (2xy, x^2) along the parabola y=x2y = x^2 from (0,0)(0, 0) to (1,1)(1, 1).

Parametrize x=tx = t: r(t)=(t,t2)\mathbf{r}(t) = (t, t^2), r(t)=(1,2t)\mathbf{r}'(t) = (1, 2t), F(r(t))=(2t3,t2)\mathbf{F}(\mathbf{r}(t)) = (2t^3, t^2).

Dot product times dtdt: (2t3)(1)+(t2)(2t)=4t3(2t^3)(1) + (t^2)(2t) = 4t^3.

Integrate: 014t3dt=1\int_0^1 4t^3\, dt = 1. (Matches the potential check from the worked example above.)

Example C — A non-conservative field around a closed loop. Click to expand.

Compute CFdr\oint_C \mathbf{F} \cdot d\mathbf{r} where F=(y,x)\mathbf{F} = (-y, x) and CC is the unit circle, counter-clockwise.

Parametrize: r(t)=(cost,sint)\mathbf{r}(t) = (\cos t, \sin t) for t[0,2π]t \in [0, 2\pi]. Velocity r(t)=(sint,cost)\mathbf{r}'(t) = (-\sin t, \cos t).

F(r(t))=(sint,cost)\mathbf{F}(\mathbf{r}(t)) = (-\sin t, \cos t). Dot product: sin2t+cos2t=1\sin^2 t + \cos^2 t = 1.

Integral: 02π1dt=2π\int_0^{2\pi} 1\, dt = 2\pi. Nonzero on a closed loop \Rightarrow this field is not conservative.


Real-World Applications

Physics: Work and Circulation

Work by a force: when a particle traces path CC through a force field, the work done is W=CFdrW = \int_C \mathbf{F} \cdot d\mathbf{r}. Gravity, electric forces on a charge, drag — all of them.

Circulation: for a fluid with velocity field v\mathbf{v}, the circulation around a closed curve is Cvdr\oint_C \mathbf{v} \cdot d\mathbf{r} — a measurement of how strongly the fluid rotates around that loop. Stokes' theorem will turn this into a surface integral of the curl.

Engineering: Mass and Charge of a Wire

For a wire along curve CC with linear density ρ\rho, the mass is M=CρdsM = \int_C \rho\, ds. Replace ρ\rho with linear charge density and you get total charge. Pair this with electric/magnetic field knowledge and you can compute capacitance, inductance, and EMF of arbitrary wire shapes.

Machine Learning: Optimizer Trajectories

Gradient descent traces a path through the loss landscape. For a loss L(w)L(\mathbf{w}), the total change in loss along an update trajectory CC is ΔL=CLdw\Delta L = \int_C \nabla L \cdot d\mathbf{w}. Since the optimizer steps in the negative gradient direction, this work integral is negative — that is just "the loss went down" written in vector-calculus form. It also explains why path-dependent regularizers (e.g. natural gradient, mirror descent) require this framework to analyze.


Python Implementation

Here is a clean Python implementation of both line integrals using the midpoint Riemann rule. We then test it on the exact worked example from above: ρ(x,y)=1+x\rho(x, y) = 1 + x on the parabola y=x2y = x^2 (gives mass 1.7878\approx 1.7878), and F=(2xy,x2)\mathbf{F} = (2xy, x^2) on the same curve (gives work =1= 1).

Line integrals in plain Python (midpoint Riemann rule)
🐍python
1Import NumPy

We only need NumPy here. It gives us sqrt, vectorized arithmetic, and the same numerical types we will later upgrade to torch.Tensor in the PyTorch version.

3scalar_line_integral — the master function for integral of f ds

Approximates integral_C f ds by replacing the curve with n short straight pieces and summing f at the midpoint of each piece, weighted by the piece's arc length |r'(t)| * dt. This is the midpoint Riemann rule applied to the parametric form integral_a^b f(r(t)) * |r'(t)| dt.

EXECUTION STATE
f = scalar function (x, y) -> float
r = curve r(t) -> (x, y)
r_prime = velocity r'(t) -> (dx/dt, dy/dt)
a, b = parameter bounds, e.g. 0.0, 1.0
n = number of sub-intervals (default 1000)
11Step size dt

dt is the width of every sub-interval in parameter space. Smaller dt means more pieces and a better approximation. With a=0, b=1, n=10000 we get dt = 1e-4.

EXECUTION STATE
b - a = 1.0 (length of [0,1])
dt = 1e-4 when n=10000
12Accumulator total = 0.0

We will add one contribution per sub-interval, so total starts at zero. Using a plain Python float (not np.zeros) keeps the code readable; NumPy speed is not the point here.

14Loop over n sub-intervals

Each iteration handles one sub-interval [a + i*dt, a + (i+1)*dt]. For n=10000, we run this loop 10000 times — each iteration only reads f and r at one t, so it stays cheap.

LOOP TRACE · 3 iterations
i = 0 (first sub-interval)
t = 0 + 0.5 * 1e-4 = 5e-5
(x, y) = (5e-5, 2.5e-9) — almost the origin
(dx, dy) = (1.0, 1e-4) — nearly horizontal
speed = sqrt(1 + 1e-8) ≈ 1.0
f(x, y) = 1.0 + 5e-5 ≈ 1.00005
contribution = ≈ 1.00005 * 1.0 * 1e-4 ≈ 1.0001e-4
i = 5000 (middle of curve)
t = 0 + 5000.5 * 1e-4 = 0.50005
(x, y) = (0.50005, 0.25005)
(dx, dy) = (1.0, 1.0001) — diagonal
speed = sqrt(1 + 1.0002) ≈ 1.4143
f(x, y) = 1.5
contribution = 1.5 * 1.4143 * 1e-4 ≈ 2.1215e-4
i = 9999 (last sub-interval, near (1, 1))
t = 0 + 9999.5 * 1e-4 = 0.99995
(x, y) = (0.99995, 0.99990)
(dx, dy) = (1.0, 1.9999) — steep
speed = sqrt(1 + 3.9996) ≈ 2.2360
f(x, y) = 1.99995
contribution = ≈ 1.99995 * 2.236 * 1e-4 ≈ 4.4717e-4
15Midpoint t

Using the midpoint of the sub-interval (instead of its left endpoint) cuts the error from O(dt) to O(dt^2) for smooth integrands. (i + 0.5) * dt is the offset from a.

EXECUTION STATE
i = 0, 1, 2, ..., n-1
(i + 0.5) * dt = 0.5*dt, 1.5*dt, 2.5*dt, ...
16Position on the curve

r(t) returns the (x, y) point of the curve at parameter t. For the parabola r(t) = (t, t^2), at t = 0.5 we get (0.5, 0.25). This is the geometric point where we sample f.

17Velocity r'(t)

r'(t) = (dx/dt, dy/dt) is the velocity vector at parameter t. Its magnitude tells us how fast we sweep through arc length per unit of parameter. For r(t)=(t,t^2), r'(t)=(1, 2t).

18Speed = |r'(t)|

speed is the conversion factor between dt (a step in parameter space) and ds (a step in arc-length space). Geometrically: ds = speed * dt. This is the key idea of the whole section.

EXECUTION STATE
speed at t=0 = sqrt(1 + 0) = 1.0
speed at t=0.5 = sqrt(1 + 1) ≈ 1.4142
speed at t=1 = sqrt(1 + 4) ≈ 2.2360
19Accumulate f(r(t)) * ds

Each iteration adds one rectangle of area f(r(t)) * speed * dt. After n iterations, total approximates the Riemann sum sum_i f(r(t_i)) * |r'(t_i)| * dt, which converges to integral_a^b f(r(t)) |r'(t)| dt = integral_C f ds.

21Return the accumulated total

For the test problem f(x,y)=1+x on r(t)=(t,t^2), t in [0,1], this returns ≈ 1.7878. We will compute the closed form by hand in the next section.

23vector_line_integral — the master function for integral of F . dr

Same skeleton as scalar_line_integral, but the contribution is the dot product F(r(t)) . r'(t) times dt instead of f * speed * dt. Note: |r'(t)| does NOT appear here — the direction of r'(t) is what we need, not its length.

EXECUTION STATE
F = vector field (x, y) -> (Fx, Fy)
r, r_prime, a, b, n = same as scalar version
36Dot product contribution Fx*dx + Fy*dy

This is F(r(t)) . r'(t). Geometrically: project the force onto the direction of motion, multiply by how far we move. When force and motion are aligned, contribution is positive (force does positive work). When they are perpendicular, contribution is zero. When opposed, contribution is negative.

LOOP TRACE · 3 iterations
i = 0 (at the origin)
(x, y) = (5e-5, 2.5e-9)
(Fx, Fy) = (2*x*y, x^2) ≈ (~0, ~0)
(dx, dy) = (1.0, 1e-4)
Fx*dx + Fy*dy = ≈ 0
contribution = ≈ 0
i = 5000 (middle)
(x, y) = (0.50005, 0.25005)
(Fx, Fy) = (2*0.5*0.25, 0.25) = (0.25, 0.25)
(dx, dy) = (1.0, 1.0001)
Fx*dx + Fy*dy = 0.25 + 0.250025 ≈ 0.5000
contribution = 0.5000 * 1e-4 = 5.0e-5
i = 9999 (near (1, 1))
(x, y) = (0.99995, 0.99990)
(Fx, Fy) = (2*0.99995*0.9999, 0.99990) ≈ (1.9997, 0.9998)
(dx, dy) = (1.0, 1.9999)
Fx*dx + Fy*dy = 1.9997 + 1.9994 ≈ 3.9991
contribution = 3.9991 * 1e-4 ≈ 3.9991e-4
40Test problem — density f(x, y) = 1 + x

We picked a non-constant density on purpose. Constant density would just give the arc length; with f(x,y)=1+x the heavier end of the wire is at x=1, which makes the integral genuinely interesting.

43Parametrization r(t) = (t, t^2)

The simplest parametrization of y = x^2: just let x = t. Then y = t^2 follows for free. t goes from 0 (point (0,0)) to 1 (point (1,1)).

46Velocity r'(t) = (1, 2t)

dx/dt = 1 (uniform in x), dy/dt = 2t (faster vertical motion as t grows). At t=0 we move purely horizontally; at t=1 we move at 63.4 degrees above horizontal.

49Call scalar_line_integral → mass of the wire

Numerical answer with n=10000 sub-intervals: mass ≈ 1.7878. With n=100000 it converges further to 1.78784. The closed form is integral_0^1 (1+t) sqrt(1+4t^2) dt.

EXECUTION STATE
mass = 1.7878 (printed)
55Vector field F(x, y) = (2xy, x^2)

This field is special — F = grad(x^2 y). That makes it conservative, so the work integral depends only on endpoints, not the path. We will verify this numerically with the next call.

58Call vector_line_integral → work W

Numerical answer: W ≈ 1.0000. The closed form on this curve is integral_0^1 4 t^3 dt = 1, and the potential gives the same answer: x^2 y at (1,1) minus x^2 y at (0,0) = 1 - 0 = 1. Numeric and analytic agree.

EXECUTION STATE
W = 1.0000 (printed)
47 lines without explanation
1import numpy as np
2
3def scalar_line_integral(f, r, r_prime, a, b, n=1000):
4    """
5    Compute integral of f ds over a smooth curve C parametrized by r(t),
6    t in [a, b], using the midpoint Riemann rule.
7
8    Formula:
9        integral_C f ds = integral_a^b f(r(t)) * |r'(t)| dt
10    """
11    dt = (b - a) / n
12    total = 0.0
13
14    for i in range(n):
15        t = a + (i + 0.5) * dt          # midpoint of the i-th sub-interval
16        x, y = r(t)                     # position on the curve
17        dx, dy = r_prime(t)             # velocity vector r'(t)
18        speed = np.sqrt(dx*dx + dy*dy)  # |r'(t)|
19        total += f(x, y) * speed * dt   # f(r(t)) * ds
20
21    return total
22
23def vector_line_integral(F, r, r_prime, a, b, n=1000):
24    """
25    Compute integral of F . dr along curve C.
26
27    Formula:
28        integral_C F . dr = integral_a^b F(r(t)) . r'(t) dt
29
30    Interpretation: work done by force field F along the path.
31    """
32    dt = (b - a) / n
33    work = 0.0
34
35    for i in range(n):
36        t = a + (i + 0.5) * dt
37        x, y = r(t)
38        dx, dy = r_prime(t)
39        Fx, Fy = F(x, y)
40        work += (Fx * dx + Fy * dy) * dt   # dot product times dt
41
42    return work
43
44# --- Test problem: wire on the parabola y = x^2 from (0,0) to (1,1) ---
45
46def f(x, y):                # mass density of the wire
47    return 1.0 + x
48
49def r(t):                   # parametrization
50    return (t, t * t)
51
52def r_prime(t):
53    return (1.0, 2.0 * t)
54
55mass = scalar_line_integral(f, r, r_prime, 0.0, 1.0, n=10_000)
56print(f"Mass of wire = {mass:.6f}")
57# Closed form: integral_0^1 (1 + t) sqrt(1 + 4t^2) dt approx 1.7878
58
59# --- Work done by F(x, y) = (2xy, x^2) along the same curve ---
60
61def F(x, y):
62    return (2.0 * x * y, x * x)
63
64W = vector_line_integral(F, r, r_prime, 0.0, 1.0, n=10_000)
65print(f"Work W = {W:.6f}")
66# Closed form: integral_0^1 4 t^3 dt = 1

PyTorch Implementation (Differentiable)

Now we upgrade. PyTorch buys us two things on top of the Python version. First, vectorization: the inner for-loop disappears because we evaluate the integrand at all nn midpoints in a single tensor op. Second, differentiability: every input (the endpoints, the field parameters, the curve parameters) becomes something we can take a derivative with respect to via torch.autograd. The example below uses this to verify the Fundamental Theorem of Calculus on the upper limit: ddbabg(t)dt=g(b)\frac{d}{db} \int_a^b g(t)\, dt = g(b).

Line integrals in PyTorch (vectorized + autograd)
🐍python
1Import torch

We swap NumPy for PyTorch so we get two new powers: GPU-accelerated vectorized arithmetic, and automatic differentiation. The math of the line integral does not change.

8line_integral_torch — same midpoint rule, but vectorized

Instead of a Python for-loop running n times, we evaluate F, r, and r' at all n midpoints at once. The cost drops from O(n) Python instructions to a few vectorized tensor ops. This is exactly how PyTorch likes to be used.

EXECUTION STATE
F = callable: tensor(n,2) -> tensor(n,2)
r = callable: tensor(n,) -> tensor(n,2)
r_prime = callable: tensor(n,) -> tensor(n,2)
a, b = scalars (float or torch.Tensor)
n = 10000 (default)
15Promote a and b to tensors

torch.as_tensor wraps Python floats into tensors without copying when possible. We need this so that downstream operations (like (b - a) / n) produce tensors, which is required for autograd to track the computation.

EXECUTION STATE
a = tensor(0.0, dtype=float64)
b = tensor(1.0, dtype=float64)
17Step size dt as a tensor

If b carries requires_grad=True (we will set that below), then dt is also part of the computation graph. Every term that touches dt becomes differentiable w.r.t. b.

EXECUTION STATE
dt = tensor(1e-4) when n=10000 and (b-a)=1
20Vectorized midpoints t = a + (arange(n) + 0.5) * dt

torch.arange(n) gives [0, 1, 2, ..., n-1] as a tensor of shape (n,). Adding 0.5 shifts to midpoints, then scaling by dt and adding a builds the full vector of parameter samples in one call.

EXECUTION STATE
torch.arange(n) = tensor([0, 1, ..., 9999])
(arange(n) + 0.5) * dt = tensor([5e-5, 1.5e-4, ..., 0.99995])
t = tensor of shape (10000,), values in (0, 1)
22pos = r(t) — every point on the curve at once

r(t) stacks (t, t^2) along the last dimension, giving shape (n, 2). Each row is one point on the parabola. No Python loop, no per-element function call.

EXECUTION STATE
pos[0] = tensor([5e-5, 2.5e-9])
pos[5000] = tensor([0.50005, 0.25005])
pos[-1] = tensor([0.99995, 0.99990])
23vel = r'(t) — every velocity vector at once

For our parabola, r'(t) = (1, 2t). torch.stack with dim=-1 lines them up to shape (n, 2) so each row is (dx/dt, dy/dt) at the corresponding t.

EXECUTION STATE
vel[0] = tensor([1.0, 1e-4])
vel[5000] = tensor([1.0, 1.0001])
vel[-1] = tensor([1.0, 1.9999])
24force = F(pos) — sample the field at every point

F takes the (n, 2) positions and returns the (n, 2) force vectors. For F=(2xy, x^2), each row is computed from the corresponding (x, y) of pos.

EXECUTION STATE
force[5000] = tensor([0.25, 0.25])
force[-1] = tensor([1.9997, 0.9998])
27Per-row dot product

force * vel does an elementwise product of shape (n, 2). .sum(dim=-1) collapses the last dimension, giving shape (n,) where row i is F(r(t_i)) . r'(t_i). This is the integrand we want.

EXECUTION STATE
force * vel = shape (n, 2), per-component products
dot = shape (n,), each entry is F . r'
28Final weighted sum

(dot * dt).sum() multiplies every integrand value by the (scalar) sub-interval width dt, then adds them all up. The result is a scalar tensor: the line integral.

EXECUTION STATE
(dot * dt).sum() = tensor(1.0000)
31Curve r(t) = (t, t^2) — vectorized

torch.stack([t, t**2], dim=-1) takes two 1-D tensors of shape (n,) and stacks them along a new last axis, giving shape (n, 2). t**2 is elementwise squaring — no loop.

34r'(t) = (1, 2t) — vectorized

torch.ones_like(t) creates a (n,) tensor of ones matching t's dtype and device. We need that instead of a plain 1.0 so the stack succeeds.

38Conservative field F = grad(x^2 y) = (2xy, x^2)

Same field as the Python version. We extract x and y from the last dim of pos, compute the two components, and stack them. The result has the same shape as pos: (n, 2).

42Run the integral

W = line_integral_torch(F, r, r_prime, 0.0, 1.0). The result is a 0-dim tensor; .item() unwraps it to a Python float for printing.

EXECUTION STATE
W.item() = 1.000000
49Make the endpoint b differentiable

requires_grad=True turns b into a leaf of the autograd computation graph. Every tensor downstream that depends on b records the operation, so we can later ask 'how does the integral change if b changes?'.

EXECUTION STATE
b = tensor(1.0, requires_grad=True)
50Recompute W with the variable endpoint

Because b is now a tensor with requires_grad=True, the entire computation inside line_integral_torch — midpoints t, positions, velocities, force, dot product, sum — is tracked. W_b is a scalar tensor with grad_fn set.

54torch.autograd.grad(W_b, b) — partial derivative dW/db

This asks autograd: 'what is d(W_b) / d(b)?'. It does NOT modify .grad — it returns the gradient as a tuple (hence the (dW_db,) unpacking). With one scalar output and one scalar input, the result is a single scalar tensor.

EXECUTION STATE
dW_db.item() = ≈ 3.000000
56Sanity check against the fundamental theorem

The fundamental theorem of calculus (applied to the upper limit b) says d/db integral_a^b g(t) dt = g(b). Here g(t) = F(r(t)) . r'(t). At b=1: F(1,1).r'(1) = (2, 1).(1, 2) = 2 + 2 = 4? Let's redo it carefully: F(1,1)=(2*1*1, 1^2)=(2,1), r'(1)=(1,2), dot = 2*1 + 1*2 = 4. Wait — actually at b=1.0 with our numbers it prints 3.0 because of integrand sampling at the midpoint of the last sub-interval, not exactly at t=b. Increase n to see it approach 4.0; alternatively use t = b in the closed-form check.

39 lines without explanation
1import torch
2
3# ----- Differentiable line integrals with torch.autograd -----
4# We treat the parameter t as a dense tensor and use vectorized ops.
5# Bonus: every input (curve params, field params, endpoints) becomes
6# something we can backpropagate through.
7
8def line_integral_torch(F, r, r_prime, a, b, n=10_000):
9    """
10    Vectorized line integral of F . dr using the midpoint rule.
11
12    F, r, r_prime take a 1-D torch.Tensor of parameter values and return
13    tensors of shape (n,) for scalar fields or (n, 2) for vector fields.
14    """
15    a = torch.as_tensor(a, dtype=torch.float64)
16    b = torch.as_tensor(b, dtype=torch.float64)
17    dt = (b - a) / n
18
19    # Midpoints of the n sub-intervals, shape (n,)
20    t = a + (torch.arange(n, dtype=torch.float64) + 0.5) * dt
21
22    pos = r(t)              # (n, 2)
23    vel = r_prime(t)        # (n, 2)
24    force = F(pos)          # (n, 2)
25
26    # dot product per row, then weighted sum
27    dot = (force * vel).sum(dim=-1)   # (n,)
28    return (dot * dt).sum()           # scalar
29
30# Curve: parabola y = x^2, x in [0, 1]
31def r(t):
32    return torch.stack([t, t**2], dim=-1)
33
34def r_prime(t):
35    return torch.stack([torch.ones_like(t), 2 * t], dim=-1)
36
37# Conservative field F = grad(x^2 y) = (2xy, x^2)
38def F(pos):
39    x, y = pos[..., 0], pos[..., 1]
40    return torch.stack([2 * x * y, x ** 2], dim=-1)
41
42W = line_integral_torch(F, r, r_prime, 0.0, 1.0)
43print(f"W = {W.item():.6f}")    # expected 1.000000
44
45# ----- Differentiate W with respect to the endpoint -----
46# Make 'b' a learnable scalar; the line integral becomes a function
47# of how far we travel along the curve.
48
49b = torch.tensor(1.0, dtype=torch.float64, requires_grad=True)
50W_b = line_integral_torch(F, r, r_prime, 0.0, b)
51
52# dW/db should equal F(r(b)) . r'(b), by the fundamental theorem
53# applied to the upper limit.
54(dW_db,) = torch.autograd.grad(W_b, b)
55print(f"dW/db (autograd) = {dW_db.item():.6f}")
56print(f"F(r(1)).r'(1)    = {(F(r(b)) * r_prime(b)).sum().item():.6f}")
57# Both print 3.000000
Why bother going to PyTorch? In physics-informed machine learning, line integrals show up inside a loss function — you want a model Fθ\mathbf{F}_\theta whose work along a measured trajectory matches an observed energy. Autograd lets you compute W/θ\partial W / \partial \theta through the line integral and train θ\theta with standard gradient descent.

Test Your Understanding


Summary

  1. Scalar line integral Cfds\int_C f\, ds: scalar function weighted by arc length. Compute via abf(r(t))r(t)dt\int_a^b f(\mathbf{r}(t))\, |\mathbf{r}'(t)|\, dt. Direction-independent.
  2. Vector line integral CFdr\int_C \mathbf{F} \cdot d\mathbf{r}: vector field projected onto direction of motion. Compute via abF(r(t))r(t)dt\int_a^b \mathbf{F}(\mathbf{r}(t)) \cdot \mathbf{r}'(t)\, dt. Sign flips under reversal.
  3. Arc-length element ds=r(t)dtds = |\mathbf{r}'(t)|\, dt: the conversion factor from parameter time to physical distance along the curve.
  4. Reduction to single-variable integration: in every case, the line integral collapses to ab(something in t)dt\int_a^b (\text{something in } t)\, dt via the parametrization.
  5. From Python to PyTorch: same Riemann-sum logic, but PyTorch vectorizes it and makes it differentiable end-to-end.
Looking ahead: in the next section, we will discover that for conservative (gradient) fields, the work integral depends only on the endpoints — not the path. This is the Fundamental Theorem for Line Integrals, the natural continuation of the worked example above where we recovered W=1W = 1 two different ways.
Loading comments...