Skip to content

Definite integrals, trapezoid, Simpson

ModuleM01.4 · build · Python · Pass 5 · 2 to 3 h
You buildpython/tinyllm/num/integrate.py: trapezoid, trapezoid_rule, simpson
Contractcourse/contracts/py/tinyllm/num/integrate.pyi
Testscourse/tests/M01.4/test_integrate.py (what they check: section 4)
Needsnothing to call. Reading: M01.1 finite differences (Richardson extrapolation, section 2.6 here)
Used byM07.7 computes ROC-AUC as trapezoid(tpr, fpr) · later ethics.04 integrates the safety report’s ROC and reliability curves the same way
MilestoneMS-P5 (the Pass 5 gate)
Optional depthOpenStax, Calculus Volume 1 (free), sections 5.1 to 5.3 (the definite integral and the fundamental theorem) and 3.6 of Volume 2 (numerical integration); Sauer, Numerical Analysis, sections 5.2 and 5.3 (Newton-Cotes rules and Romberg)
  • The definite integral ∫abf(x) dx\int_a^b f(x)\,dx is the signed area under ff, the limit of sums of thin slices; a computer stops at nn slices of width hh (test_hand_example).
  • The trapezoid rule joins neighbouring points by straight lines: error proportional to h2h^2, exact on lines (test_trapezoid_error_order_two, test_trapezoid_rule_exact_on_lines).
  • Simpson’s rule fits a parabola through each three points: error proportional to h4h^4 and, by a symmetry bonus, exact on cubics (test_simpson_error_order_four, test_simpson_exact_on_cubics).
  • Simpson is Richardson extrapolation of the trapezoid rule: (4T(h/2)−T(h))/3(4T(h/2) - T(h))/3, the same error-cancelling step as M01.1 (test_simpson_is_richardson_of_trapezoid).
  • Samples that are not on a grid, such as the corners of an ROC curve, are integrated step by step with each step’s own signed width (test_trapezoid_samples_match_scipy).
Terminal window
ol start M01.4 # stubs python/tinyllm/num/integrate.py into your repo
ol tests M01.4 # read the test catalog first: rung R0, you write no tests here
ol check M01.4 # exit code is the verdict
ol diff M01.4 # after passing: your code against the reference

Pass 5 trains classifiers for the first time: BERT and ELECTRA heads in L6.3 and L6.5, and the usage-policy head the gateway will run (D33). A classifier outputs a score, and the question “how good is this score at separating the two classes?” has a standard answer that does not depend on any one threshold: the area under the ROC curve, which M07.7 computes. That curve is not a formula; it is a list of corner points, one per threshold, and its area is a definite integral over samples. The same integral computes the expected calibration area in the safety report (ethics.04). Get the integration rule subtly wrong (count each step at its left height, or sort points that were deliberately ordered) and every AUC in the model zoo is biased, with nothing visibly broken. This module builds the area rule for samples and, for functions you can evaluate anywhere, the two classic rules with predictable errors.

SymbolMeaningType / shape
ffa function of one real variable, vectorized over float64 arraysCallable[[NDArray], NDArray]
[a,b][a, b]the interval of integration; b<ab < a is allowedfloat, float
∫abf(x) dx\int_a^b f(x)\,dxthe definite integral: the signed area between ff and the xx axisfloat
FFan antiderivative of ff: F′=fF' = ffunction
nnthe number of equal stepsint
hhthe step, (b−a)/n(b - a)/nfloat
xix_ithe nodes a+iha + ih, i=0,…,ni = 0, \ldots, n (numpy.linspace(a, b, n + 1))float64[n + 1]
fif_if(xi)f(x_i)float64[n + 1]
T(h)T(h), S(h)S(h)the composite trapezoid and Simpson values with step hhfloat
O(hp)O(h^p)at most a constant times hph^p as h→0h \to 0

Cut [a,b][a, b] into nn slices of width hh, stand a rectangle on each slice with the height of ff somewhere in it, and add the areas: ∑if(ξi) h\sum_i f(\xi_i)\, h. This is a Riemann sum. As h→0h \to 0 the sums for a continuous ff settle on one number whatever points ξi\xi_i you chose, and that number is the definite integral ∫abf(x) dx\int_a^b f(x)\,dx. Area below the axis counts as negative, which is why it is a signed area. Running from bb to aa makes every width −h-h, so ∫baf=−∫abf\int_b^a f = -\int_a^b f, and an interval of length 0 has integral 0.

The fundamental theorem of calculus links this area to derivatives (M01.1): if F′=fF' = f, then ∫abf(x) dx=F(b)−F(a)\int_a^b f(x)\,dx = F(b) - F(a). For f(x)=x3f(x) = x^3, F(x)=x4/4F(x) = x^4/4, so ∫02x3 dx=16/4−0=4\int_0^2 x^3\,dx = 16/4 - 0 = 4. Most integrals you meet in practice have no FF in closed form (e−x2/2e^{-x^2/2}, the normal density, is the famous one) or ff is only known at sample points. Then you compute the area numerically, which is called quadrature.

Replace ff on one step [xi,xi+1][x_i, x_{i+1}] by the straight line through its two end values. The region under that line is a trapezoid with parallel sides fif_i and fi+1f_{i+1} and width hh, so its area is h (fi+fi+1)/2h\,(f_i + f_{i+1})/2: the width times the average height. A straight line is integrated exactly, because the “approximation” is the function itself.

Add one trapezoid per step. Every inner node belongs to two trapezoids and is counted twice at half weight; the two end nodes belong to one each:

T(h)=h(12f0+f1+f2+⋯+fn−1+12fn).T(h) = h\left(\tfrac{1}{2} f_0 + f_1 + f_2 + \cdots + f_{n-1} + \tfrac{1}{2} f_n\right).

Simpson’s rule instead takes the steps in pairs and fits the parabola through the three points (x2j,x2j+1,x2j+2)(x_{2j}, x_{2j+1}, x_{2j+2}). The area under that parabola, over a panel of width 2h2h, is h3(f2j+4f2j+1+f2j+2)\frac{h}{3}(f_{2j} + 4 f_{2j+1} + f_{2j+2}) (integrate the parabola through (−h,f−),(0,f0),(h,f+)(-h, f_-), (0, f_0), (h, f_+) term by term to check it). Adding the panels, every even inner node is shared by two panels:

S(h)=h3(f0+4f1+2f2+4f3+2f4+⋯+2fn−2+4fn−1+fn).S(h) = \frac{h}{3}\left(f_0 + 4 f_1 + 2 f_2 + 4 f_3 + 2 f_4 + \cdots + 2 f_{n-2} + 4 f_{n-1} + f_n\right).

The pattern 1, 4, 2, 4, …, 2, 4, 1 only closes when nn is even. An odd nn is not a smaller Simpson’s rule; it is a different, wrong one, so the contract raises instead of rounding.

An ROC curve is a list of points (xi,yi)(x_i, y_i) with uneven gaps, produced by sweeping a threshold. Its area is the trapezoid rule applied step by step with each step’s own width: ∑i(xi+1−xi)(yi+yi+1)/2\sum_i (x_{i+1} - x_i)(y_i + y_{i+1})/2. The widths keep their sign, so the order of the points is the direction of travel: the same points in decreasing order give the negative area, exactly as running an integral from bb to aa. The function never sorts, because sorting would silently reconnect the points in a different order (the curve the caller drew is the curve that gets measured). Fewer than two points enclose nothing: the area is 0.

Expand ff around the middle of one step with Taylor’s formula (M01.1 section 2.2, proved in M02.1). The straight line misses the curvature term, and integrating the difference over one step gives an error of −h312f′′(ξ)-\frac{h^3}{12} f''(\xi) for some ξ\xi in the step. There are n=(b−a)/hn = (b - a)/h steps, so the composite error is

∫abf−T(h)=−(b−a) h212 f′′(ξ),∫abf−S(h)=−(b−a) h4180 f(4)(ξ).\int_a^b f - T(h) = -\frac{(b - a)\, h^2}{12}\, f''(\xi), \qquad \int_a^b f - S(h) = -\frac{(b - a)\, h^4}{180}\, f^{(4)}(\xi).

The trapezoid rule is second order: halve hh and the error drops by 4. Simpson is fourth order: halve hh and it drops by 16. A parabola should only be exact on quadratics, yet the error involves f(4)f^{(4)}, so Simpson is exact on cubics too: the x3x^3 error on the left half of a panel cancels the one on the right half by symmetry. On ∫0πsin⁡x dx=2\int_0^\pi \sin x\,dx = 2:

nn2481632
trapezoid error4.3×10−14.3 \times 10^{-1}1.0×10−11.0 \times 10^{-1}2.6×10−22.6 \times 10^{-2}6.4×10−36.4 \times 10^{-3}1.6×10−31.6 \times 10^{-3}
Simpson error9.4×10−29.4 \times 10^{-2}4.6×10−34.6 \times 10^{-3}2.7×10−42.7 \times 10^{-4}1.7×10−51.7 \times 10^{-5}1.0×10−61.0 \times 10^{-6}

Each column divides the trapezoid error by 4 and the Simpson error by 16: the slopes 2 and 4 the order tests measure. The error formulas assume a smooth ff at the scale of hh. On Runge’s peak 1/(1+25x2)1/(1 + 25x^2) over [−1,1][-1, 1] with n=8n = 8, Simpson’s error is 0.026 and the trapezoid’s 0.0075: the parabolas overshoot a peak that is narrower than two steps. Higher order pays off only once hh resolves the function.

The trapezoid error is not just O(h2)O(h^2); it is a series in even powers, T(h)=I+c2h2+c4h4+⋯T(h) = I + c_2 h^2 + c_4 h^4 + \cdots (the Euler-Maclaurin formula). That is the same shape as the central difference of M01.1, so the same step removes the leading term:

4 T(h)−T(2h)3=I+O(h4).\frac{4\,T(h) - T(2h)}{3} = I + O(h^4).

Write it out node by node and the weights come out as 1, 4, 2, 4, …, 1 over 3: it is Simpson’s rule on the finer grid. Repeating the step (weights 16,64,…16, 64, \ldots) is Romberg integration. The test test_simpson_is_richardson_of_trapezoid checks this identity to rounding.

Take ∫02x3 dx=4\int_0^2 x^3\,dx = 4.

RuleNodes and valuesComputationValueError
TT, n=2n = 2, h=1h = 1f(0,1,2)=0,1,8f(0, 1, 2) = 0, 1, 81⋅(0/2+1+8/2)1 \cdot (0/2 + 1 + 8/2)51
TT, n=4n = 4, h=0.5h = 0.5f(0,0.5,1,1.5,2)=0,0.125,1,3.375,8f(0, 0.5, 1, 1.5, 2) = 0, 0.125, 1, 3.375, 80.5⋅(0+0.125+1+3.375+4)0.5 \cdot (0 + 0.125 + 1 + 3.375 + 4)4.250.25
SS, n=2n = 2, h=1h = 10,1,80, 1, 813(0+4⋅1+8)\frac{1}{3}(0 + 4 \cdot 1 + 8)40
RichardsonT(0.5)T(0.5), T(1)T(1)(4⋅4.25−5)/3=12/3(4 \cdot 4.25 - 5)/3 = 12/340

Halving hh divided the trapezoid error by exactly 4 (here f′′=6xf'' = 6x and the error formula is exact up to the mean value). Simpson is exact because x3x^3 is a cubic, and Richardson’s combination of the two trapezoid values gives the same 4.

Now three samples, the corners of an ROC curve: (0,0)(0, 0), (0.5,0.75)(0.5, 0.75), (1,1)(1, 1). Two trapezoids: 0.5⋅(0+0.75)/2=0.18750.5 \cdot (0 + 0.75)/2 = 0.1875 and 0.5⋅(0.75+1)/2=0.43750.5 \cdot (0.75 + 1)/2 = 0.4375, total 0.625. Counting each step at its left height only (a left Riemann sum) gives 0.5⋅0+0.5⋅0.75=0.3750.5 \cdot 0 + 0.5 \cdot 0.75 = 0.375. These numbers are the first two tests, test_hand_example and test_hand_example_samples.

def trapezoid(y: ArrayLike, x: ArrayLike) -> float: ...
def trapezoid_rule(f: Callable[[NDArray], NDArray], a: float, b: float, n: int) -> float: ...
def simpson(f: Callable[[NDArray], NDArray], a: float, b: float, n: int) -> float: ...

trapezoid integrates samples in the order given (numpy’s trapezoid(y, x)). The two rules evaluate f once on numpy.linspace(a, b, n + 1), so both end nodes are exact and every implementation agrees with scipy to the last bit. All three return a Python float and raise ValueError for non-finite input, mismatched samples, a wrongly shaped or non-finite f, n < 1, or (for Simpson) an odd n.

TestKINDChecksWhy it matters downstream
test_hand_exampleunit, smokesection 3: 5, 4.25, 4, 4you and the tests agree on the rules
test_hand_example_samplesunit, smokethe ROC corners give 0.625roc_auc in M07.7 is this call
test_rules_match_scipygoldenscipy.integrate.trapezoid and simpson on the same nodes, six functions and a reversed intervalthe rules are the standard ones
test_trapezoid_samples_match_scipygoldenuneven, unsorted, and decreasing samplesROC and reliability curves
test_trapezoid_error_order_twopropertylog-log error slope 2section 2.5
test_simpson_error_order_fourpropertylog-log error slope 4section 2.5
test_simpson_exact_on_cubicsproperty40 random cubics, n=2,4,10n = 2, 4, 10the symmetry bonus
test_trapezoid_rule_exact_on_linespropertyexact on lines, not on x2x^2section 2.2
test_simpson_is_richardson_of_trapezoidpropertyS=(4T(h)−T(2h))/3S = (4T(h) - T(2h))/3 to roundingsection 2.6
test_reversed_and_empty_intervalsboundary∫ba=−∫ab\int_b^a = -\int_a^b, ∫aa=0\int_a^a = 0, decreasing samples negativesigned areas
test_f_called_once_on_linspace_nodesunitone vectorized call on the contract’s nodesbit-identical results across languages
test_returns_python_floatunita float, not a numpy scalarcallers compare and serialize it
test_trapezoid_short_inputsboundary, smokezero or one sample gives 0.0a one-threshold ROC curve
test_rejects_bad_argumentsboundaryodd or non-positive n, non-finite limits, bad f, bad samples raisebugs surface at the call
PitfallSymptomCaught by
1. a Riemann sum (left heights only) for samplesthe hand ROC area is 0.375, not 0.625; AUCs biased lowtest_hand_example_samples (mutant s01)
2. Simpson weights 2 and 4 swappedorder 2 instead of 4; the hand example gives 10/3test_hand_example, test_simpson_exact_on_cubics (mutant s02)
3. accepting an odd number of Simpson stepsa plausible but wrong numbertest_rejects_bad_arguments (mutant s03)
4. counting the end points fully in the trapezoid rulethe hand example gives 9, order 1test_hand_example (mutant s04)
5. sorting samples or using ∣Δx∣\lvert \Delta x \rverta decreasing curve has positive area; unsorted samples are reconnectedtest_trapezoid_samples_match_scipy (mutant s05)
6. nn nodes instead of n+1n + 1the last step is lost; the rules disagree with scipytest_rules_match_scipy, test_f_called_once_on_linspace_nodes (mutant s06)
7. Simpson with the panel width 2h2h in place of the step hhevery result doubledtest_hand_example (mutant s07)
8. assuming equal gaps between sampleswrong area on any real ROC curvetest_trapezoid_samples_match_scipy (mutant s08)
DirectionModuleHow it uses this
BackM01.1Richardson extrapolation and the Taylor error terms of section 2.5 (reading)
ForwardM07.7roc_auc(scores, labels) builds the ROC corners and returns trapezoid(tpr, fpr)
Forwardethics.04the safety report’s ROC area and reliability curve
ForwardS-M07dq8 computes an AUC by hand, the same area as pairs ranked correctly

If you skip this module, ol check M07.7 stops with BLOCKED ... needs M01.4: build it, or pass --ref-deps.

Your pieceProduction equivalentWhat it addsWhere to look
trapezoidnumpy.trapezoid, scipy.integrate.trapezoidintegration along any axis of an N-d arraynumpy/lib/_function_base_impl.py
simpsonscipy.integrate.simpsonuneven sample spacing and an odd number of intervals (a corrected last panel)scipy/integrate/_quadrature.py
simpson with RichardsonRomberg integration, scipy.integrate.quadadaptive Gauss-Kronrod: more nodes only where the error estimate is largeQUADPACK (qags)
roc_auc via trapezoidsklearn.metrics.roc_auc_scorethe same corners and trapezoids, plus multiclass averagingsklearn/metrics/_ranking.py (auc)