Two colleagues collaborating at a computer workstation in an open-plan office with other employees working nearby.

Keep NumPy interp from Hiding Out-of-Range Queries

Fri, Oct 30, 2026

An unchanged query coordinate of -1 can return the finite value 10 from numpy.interp even when the tabulated sample coordinates run only from 0 through 5. Nothing has crashed. Nothing is NaN. NumPy is doing what its API documents: unless different left and right values are supplied, coordinates below the first knot receive the first tabulated value and coordinates above the last knot receive the last value.

That behavior is useful when constant endpoint extension is actually the intended model. The operational risk appears when a downstream check interprets “finite result” as “query was interpolated inside the sampled interval.” Those are different claims.

This article builds a bounded laboratory around one synthetic, dimensionless float64 table, an immutable seven-row query list, NumPy 2.3.5 and the supplied Python 3.12.14 reference environment. It separates three things that numerical pipelines often compress into one output array: the library's documented endpoint-fill behavior, exact numerical-domain admission, and an independently authored policy for values outside that domain.

The intended reader is a data engineer, analyst or numerical Python maintainer responsible for a one-dimensional tabulated transformation. The goal is not to establish real calibration accuracy or permission to extrapolate. It is to leave every submitted query auditable as an in-domain estimate, a held outside-domain query, an invalid query, or a separately declared constant extension.

Define the table's numerical domain

The fixture starts with three knots and values:

xp = [0.0, 2.0, 5.0]
fp = [10.0, 14.0, 8.0]

The numerical interpolation domain is therefore the inclusive interval [0, 5]. The seven submitted coordinates are [-1, 0, 1, 2, 3.5, 5, 6], with immutable IDs below, first, inside_a, middle, inside_b, last, and above.

These values are synthetic and dimensionless. There is no evidence here assigning them physical units, calibration authority, measurement uncertainty or a scientifically justified operating range. That distinction matters. Numerical interval membership answers a narrow computational question: is a finite query coordinate between the first and last admitted knot, endpoints included? It does not establish that the table is valid for a real process, that the knots were measured correctly, or that a piecewise-linear model is scientifically adequate.

The same separation applies to the outputs. At q=1, the fixture's model gives 12; at q=3.5, it gives 11. Those are computed estimates under an authored interpolation model, not observations.

Preserving the submitted query is therefore part of the evidence. Rewriting -1 as 0, dropping the row, or replacing its identity would answer a different question. That general provenance principle is consistent with a practical Python data-cleaning workflow, which emphasizes preserving source inputs and retaining evidence about transformations rather than overwriting meaning during cleanup.

For this laboratory, IN_DOMAIN means only: finite query, q >= xp[0], and q <= xp[-1]. It deliberately means nothing more.

Separate interpolation from endpoint extension

NumPy documents numpy.interp as a one-dimensional piecewise-linear interpolator for increasing sample coordinates. With period omitted, xp is expected to increase; the documentation warns that non-increasing coordinates produce meaningless interpolation results and notes that xp cannot contain NaN.

Inside [0,5], this fixture has two linear pieces. From (0,10) to (2,14), the slope is 2, so the function is

f(q) = 10 + 2q       for 0 <= q <= 2

From (2,14) to (5,8), the slope is -2, so

f(q) = 18 - 2q       for 2 <= q <= 5

Those equations make the expected in-domain values transparent: 10, 12, 14, 11, 8 at q = 0, 1, 2, 3.5, 5.

Outside the interval, the default behavior is different. It is not evaluation of either line beyond its segment.

Read left and right as fill policies

For x < xp[0], NumPy says left defaults to fp[0]; for x > xp[-1], right defaults to fp[-1]. Consequently, this fixture returns 10 for -1 and 8 for 6 if those queries are passed directly to np.interp without explicit outside fills.

SciPy's interpolation guide describes the same behavior as constant endpoint extension and uses it as an example of why extrapolation controls differ between interpolation routines. That documentation is useful context, but its example does not establish constant extension as a scientifically appropriate default for an arbitrary application.

The operational distinction is:

Coordinate state

Numerical action

Meaning in this laboratory

0 <= q <= 5

Piecewise-linear interpolation

IN_DOMAIN

q < 0 with NumPy default

Return fp[0]

Documented endpoint fill, not in-domain interpolation

q > 5 with NumPy default

Return fp[-1]

Documented endpoint fill, not in-domain interpolation

Outside with explicit approved policy

Return boundary value

DECLARED_CONSTANT_EXTENSION

Outside under hold policy

No numerical result retained

HELD_OUTSIDE

The scope here is deliberately nonperiodic. When period is supplied, NumPy normalizes periodic coordinates and ignores left and right, which changes the domain contract and falls outside this laboratory.

Pin the environment and expected records

The supplied reference execution records Python 3.12.14 and NumPy 2.3.5. The NumPy API documentation used here is the 2.3 branch. The Python fractions and math documentation is from the 3.12 branch, whose currently served patch documentation can differ from the executed 3.12.14 interpreter. These documentation versions and the recorded runtime should not be conflated into a patch-specific feature claim.

With Python 3.12.14 installed as python3.12, the reproduction sequence is:

python3.12 --version
python3.12 -m venv .venv-interp
.venv-interp/bin/python -m pip install numpy==2.3.5
.venv-interp/bin/python interp_domain_lab.py > interp-domain-evidence.json

The source SHA-256 supplied with the reference evidence is:

d866475a3302a28b693ebb56a75802912e2a06acc192169fec99273290b24000

That digest identifies the supplied source bytes; by itself it does not authenticate who executed them.

Before looking at NumPy's results, establish the intended hold vector:

[None, 10.0, 12.0, 14.0, 11.0, 8.0, None]

The two None positions correspond to below and above, not to failed arithmetic. The independent scalar oracle computes only coordinates in [0,5] from literal rational knots. Python's Fraction can represent the exact value of a supplied binary float; constructing Fraction from a float does not reconstruct some earlier decimal token that may have produced it.

Readers needing broader environment context can consult Refonte Learning's overview of Python libraries used in data-science workflows, but this laboratory pins the dependency that matters rather than relying on an unqualified “current NumPy” assumption.

Run the complete twelve-case laboratory

Save the following supplied source as interp_domain_lab.py. It is intentionally self-contained: SciPy is not an executed dependency, and the helper admits only a narrowly defined teaching representation.

"""Owned, bounded interpolation-domain lab; no real calibration or publication."""
import hashlib
import json
import math
import platform
import sys
from datetime import datetime, timezone
from fractions import Fraction
from pathlib import Path

import numpy as np

XP = np.array([0., 2., 5.])
FP = np.array([10., 14., 8.])
QUERIES = np.array([-1., 0., 1., 2., 3.5, 5., 6.])
IDS = ["below", "first", "inside_a", "middle", "inside_b", "last", "above"]
EXPECTED = [None, 10., 12., 14., 11., 8., None]
HELPER_CALLS = 0


def require(condition, message):
    if not condition:
        raise AssertionError(message)


def vector(values):
    return np.array(values, dtype=np.float64)


def plain(values):
    return [float(x) if math.isfinite(float(x)) else None for x in values]


def rational_value(q):
    # Literal knots and scalar arithmetic, independent of NumPy interpolation.
    q = Fraction(float(q))
    knots = [(Fraction(0), Fraction(10)), (Fraction(2), Fraction(14)),
             (Fraction(5), Fraction(8))]
    for (a, fa), (b, fb) in zip(knots, knots[1:]):
        if a <= q <= b:
            return float(fa + (q - a) (fb - fa) / (b - a))
    return None


def bounded_interp(xp, fp, ids, queries, policy="hold"):
    global HELPER_CALLS
    for a in (xp, fp, queries):
        if type(a) is not np.ndarray or a.ndim != 1 or a.dtype != np.dtype("float64"):
            raise ValueError("Expected ordinary float64 vectors")
    if (len(xp) < 2 or len(xp) != len(fp) or not np.isfinite(xp).all()
            or not np.isfinite(fp).all() or np.any(np.abs(xp) > 100)
            or np.any(np.abs(fp) > 100) or not np.all(np.diff(xp) > 0)):
        raise ValueError("Invalid bounded calibration table")
    if (type(ids) is not list or len(ids) != len(queries)
            or not all(type(i) is str and i for i in ids) or len(set(ids)) != len(ids)):
        raise ValueError("Invalid query identities")
    if policy not in ("hold", "constant"):
        raise ValueError("Unknown outside-domain policy")
    finite = np.isfinite(queries)
    inside = finite & (queries >= xp[0]) & (queries <= xp[-1])
    result = np.full(queries.shape, np.nan)
    calls = 0
    if inside.any():
        calls += 1
        HELPER_CALLS += 1
        result[inside] = np.interp(queries[inside], xp, fp)
    if policy == "constant":
        result[finite & (queries < xp[0])] = fp[0]
        result[finite & (queries > xp[-1])] = fp[-1]
    records = []
    for i, q, valid, included, y in zip(ids, queries, finite, inside, result, strict=True):
        state = ("INVALID_QUERY" if not valid else "IN_DOMAIN" if included else
                 "HELD_OUTSIDE" if policy == "hold" else "DECLARED_CONSTANT_EXTENSION")
        records.append({"id": i, "query_repr": repr(float(q)), "query_hex": float(q).hex(),
                        "state": state, "value": None if math.isnan(float(y)) else float(y)})
    require(all(r["value"] is None or math.isfinite(r["value"]) for r in records), "Nonfinite output")
    return {"policy": policy, "records": records, "helper_interp_calls": calls,
            "in_domain_count": int(inside.sum())}


def main(report):
    require(np.__version__ == "2.3.5", "NumPy baseline mismatch")
    original = [a.copy() for a in (XP, FP, QUERIES)]
    rows = report["cases"]

    def record(case, observed, expected, decision):
        require(observed == expected, case + " mismatch")
        rows.append({"case": case, "observed": observed, "expected": expected,
                     "decision": decision, "status": "PASS"})

    raw = np.interp(QUERIES, XP, FP)
    record("I01_default_extension", [plain(raw), bool(np.isfinite(raw).all())],
           [[10., 10., 12., 14., 11., 8., 8.], True], "FINITE_OUTPUT_IS_NOT_DOMAIN_ACCEPTANCE")
    filled = np.interp(QUERIES, XP, FP, left=np.nan, right=np.nan)
    record("I02_explicit_nan_fill", plain(filled), EXPECTED, "RETAIN_DOMAIN_EVIDENCE_WITH_VALUES")
    oracle = [rational_value(q) for q in QUERIES]
    accepted = bounded_interp(XP, FP, IDS, QUERIES)
    record("I03_hold_with_complete_ledger", [oracle, [r["value"] for r in accepted["records"]],
           [r["state"] for r in accepted["records"]]], [EXPECTED, EXPECTED,
           ["HELD_OUTSIDE"] + ["IN_DOMAIN"] 5 + ["HELD_OUTSIDE"]], "ACCEPT_BOUNDED_IMPLEMENTATION")
    report["main_ledger"] = accepted
    clipped = np.clip(QUERIES, XP[0], XP[-1])
    record("I04_clipped_queries", [plain(np.interp(clipped, XP, FP)),
           [i for i, a, b in zip(IDS, QUERIES, clipped, strict=True) if a != b]],
           [plain(raw), ["below", "above"]], "HOLD_UNAPPROVED_QUERY_CHANGE")
    extended = bounded_interp(XP, FP, IDS, QUERIES, "constant")
    record("I05_declared_constant_policy", [[r["value"] for r in extended["records"]],
           [r["id"] for r in extended["records"] if r["state"] == "DECLARED_CONSTANT_EXTENSION"]],
           [plain(raw), ["below", "above"]], "EXTENSION_IS_SEPARATE_AUTHORED_POLICY")
    report["constant_policy_ledger"] = extended
    edges = vector([math.nextafter(0., -math.inf), 0., 5., math.nextafter(5., math.inf)])
    edge_result = bounded_interp(XP, FP, ["just_below", "lower", "upper", "just_above"], edges)
    record("I06_exact_domain_edges", [r["state"] for r in edge_result["records"]],
           ["HELD_OUTSIDE", "IN_DOMAIN", "IN_DOMAIN", "HELD_OUTSIDE"], "KEEP_INCLUSIVE_EXACT_BOUNDARIES")
    report["edge_ledger"] = edge_result
    invalid_queries = bounded_interp(XP, FP, ["nan", "positive_inf", "negative_inf", "ok"],
                                     vector([math.nan, math.inf, -math.inf, 1.]))
    record("I07_nonfinite_queries", [r["state"] for r in invalid_queries["records"]],
           ["INVALID_QUERY"] * 3 + ["IN_DOMAIN"], "RETAIN_INVALID_QUERY_IDENTITIES")
    report["invalid_query_ledger"] = invalid_queries

    def rejected(candidates):
        held = []
        for label, xp, fp, ids, queries in candidates:
            before = HELPER_CALLS
            try:
                bounded_interp(xp, fp, ids, queries)
            except ValueError:
                held.append(label)
            else:
                raise AssertionError("Missing admission hold: " + label)
            require(HELPER_CALLS == before, "Rejected input reached interpolation: " + label)
        return held

    order_cases = [("unsorted", vector([0, 5, 2]), FP, IDS, QUERIES),
                   ("duplicate", vector([0, 2, 2]), FP, IDS, QUERIES)]
    record("I08_invalid_knot_order", rejected(order_cases), ["unsorted", "duplicate"], "HOLD_TABLE_BEFORE_INTERPOLATION")
    invalid_tables = [("nan_xp", vector([0, math.nan, 5]), FP, IDS, QUERIES),
                      ("infinite_fp", XP, vector([10, math.inf, 8]), IDS, QUERIES),
                      ("length", XP, vector([10, 14]), IDS, QUERIES),
                      ("shape", XP[:, None], FP, IDS, QUERIES),
                      ("dtype", XP.astype(np.int64), FP, IDS, QUERIES),
                      ("one_knot", vector([0]), vector([10]), IDS, QUERIES)]
    record("I09_invalid_table_representation", rejected(invalid_tables),
           ["nan_xp", "infinite_fp", "length", "shape", "dtype", "one_knot"], "HOLD_INVALID_TABLE")
    repeat = bounded_interp(XP, FP, ["r1", "r2"], vector([1, 1]))
    identity_holds = rejected([("duplicate_ids", XP, FP, ["r1", "r1"], vector([1, 1])),
                               ("id_length", XP, FP, ["r1"], vector([1, 1]))])
    record("I10_query_identity", [[[r["id"], r["value"]] for r in repeat["records"]], identity_holds],
           [[["r1", 12.], ["r2", 12.]], ["duplicate_ids", "id_length"]], "PRESERVE_DISTINCT_REPEATED_QUERIES")
    order = [6, 2, 0, 5, 1, 4, 3]
    shuffled = bounded_interp(XP, FP, [IDS[i] for i in order], QUERIES[order])
    record("I11_query_reordering", [[r["id"], r["value"]] for r in shuffled["records"]],
           [[IDS[i], EXPECTED[i]] for i in order], "PRESERVE_SUBMITTED_ORDER_AND_IDENTITY")
    empty = bounded_interp(XP, FP, [], vector([]))
    outside = bounded_interp(XP, FP, ["left", "right"], vector([-1, 6]))
    record("I12_no_in_domain_queries", [empty["records"], empty["helper_interp_calls"],
           outside["helper_interp_calls"], [r["state"] for r in outside["records"]]],
           [[], 0, 0, ["HELD_OUTSIDE", "HELD_OUTSIDE"]], "NO_ACCEPTED_VALUES_IS_NOT_DATA_COVERAGE")
    require(all(np.array_equal(a, b) for a, b in zip((XP, FP, QUERIES), original)), "Source mutation")
    require([r["case"][:3] for r in rows] == [f"I{i:02d}" for i in range(1, 13)], "Incomplete case inventory")
    require(HELPER_CALLS == 6, "Unexpected helper interpolation-call inventory")
    report.update(status="PASS_12_CASES", source_inputs_preserved=True, helper_interp_calls=HELPER_CALLS)


if name == "__main__":
    report = {"status": "FAIL", "cases": [], "started_utc": datetime.now(timezone.utc).isoformat(),
              "source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
              "environment": {"python": sys.version, "numpy": np.__version__, "platform": platform.platform()},
              "input": {"xp": XP.tolist(), "fp": FP.tolist(), "ids": IDS, "queries": QUERIES.tolist()}}
    try:
        main(report)
    except Exception as error:
        report["failure"] = {"type": type(error).__name__, "message": str(error)}
    report["finished_utc"] = datetime.now(timezone.utc).isoformat()
    print(json.dumps(report, indent=2, allow_nan=False))
    sys.exit(0 if report["status"] == "PASS_12_CASES" else 1)

Read the reference run without overstating it

The supplied reference evidence says this source ran on October 30, 2026, from 07:42:47.109626 to 07:42:47.118098 UTC, under Python 3.12.14, NumPy 2.3.5 and Linux-6.18.57-x86_64-with-glibc2.39, with Clang 22.1.3 recorded in the interpreter details. It exited with status 0, reported PASS_12_CASES, preserved the source arrays and counted six calls made through the bounded helper. Those timestamps are execution metadata, not performance evidence.

The program begins its JSON status as FAIL. If an assertion or other exception occurs, it records the error and exits nonzero because the final sys.exit returns success only when status became PASS_12_CASES. A rerun should therefore be compared on decisions, expected values and case inventory, not byte-for-byte JSON: timestamps and platform strings can legitimately differ.

The compact reference ledger is:

Case

Supplied reference observation

Decision represented by the test

I01

[10,10,12,14,11,8,8], all finite

Finiteness is not domain acceptance

I02

[null,10,12,14,11,8,null]

Preserve outside evidence

I03

Oracle and helper agree; 5 inside, 2 held

Accept bounded hold implementation

I04

Clipping reproduces I01 values; 2 coordinates changed

Hold unapproved input change

I05

Same numerical vector, outside IDs explicitly extended

Keep extension as separate policy

I06

Nearest outside floats held; 0 and 5 inside

Preserve exact inclusive boundaries

I07

NaN and infinities invalid; 1 inside

Retain invalid query identities

I08

Unsorted and duplicate knots rejected

Hold before helper interpolation

I09

Six malformed table representations rejected

Hold invalid table

I10

Repeated 1s with distinct IDs both produce 12

Preserve row identity

I11

Shuffled IDs retain their associated results

Verify by identity, not sorted position

I12

Empty and all-outside batches make zero helper calls

No accepted values does not imply coverage

The six-call count refers only to bounded_interp. It excludes the intentional direct np.interp calls in I01, I02 and I04. It would therefore be incorrect to claim that every interpolation call in the whole script receives only in-domain coordinates. The narrower, supported statement is that the helper calls np.interp only on its inside slice.

Explain why finite outputs pass the wrong check

I01 demonstrates the operational trap directly. np.interp(QUERIES, XP, FP) returns seven finite numbers, including 10 for -1 and 8 for 6. NumPy documents those two values as its default left and right behavior.

np.isfinite answers a different question: whether a value is neither infinity nor NaN. It does not test whether a query was inside [xp[0], xp[-1]], whether a table was scientifically authorized there, or whether constant extension was approved.

I02 makes the outside positions more visible by calling:

np.interp(QUERIES, XP, FP, left=np.nan, right=np.nan)

The result becomes [NaN, 10, 12, 14, 11, 8, NaN], serialized by the fixture's plain conversion as [null,10,12,14,11,8,null]. This is a useful diagnostic control because outside coordinates cannot masquerade as valid endpoint-valued numbers.

It is still not a complete audit record. A missing value alone does not say whether a query was outside, nonfinite, rejected for some other reason, or intentionally withheld. That is why the helper records identity and state separately from value.

This problem is distinct from Refonte Learning's article on NumPy evaluation and output-buffer validation, which examines whether invalid arithmetic happens before selection and who owns an output buffer. Here, the numerical call can succeed exactly as documented while the semantic classification remains wrong.

Compare a hold with declared constant extension

I03, I04 and I05 produce the central comparison.

Under the default hold policy, below and above retain their original coordinates and IDs but have no numerical value. Their states are HELD_OUTSIDE. The five other records are IN_DOMAIN and agree with the independent oracle.

I05 keeps those same original outside coordinates but applies a separately declared "constant" policy. below receives 10, above receives 8, and both are labelled DECLARED_CONSTANT_EXTENSION. Numerically, that seven-value vector equals NumPy's default result. Semantically, it is different because the ledger says why the outside values exist.

Treat extension approval as an application decision

The string "constant" in this fixture is authored metadata. It demonstrates how a pipeline can represent an explicit policy; it is not independent evidence that a real domain owner approved that policy.

I04 exposes an even subtler equivalence. np.clip(QUERIES, 0, 5) transforms -1 to 0 and 6 to 5. NumPy documents clipping as replacing values below and above an interval with the interval edges. Interpolating those modified coordinates therefore gives exactly the same numerical vector as default endpoint filling.

But the question changed. The submitted query “evaluate at -1” became “evaluate at 0.” Identical output values do not establish identical provenance.

That is why a release decision should distinguish at least three routes to the same number: default library fill, separately approved constant extension, and interpolation after coordinate modification. Only the second route records an explicit outside-domain model without rewriting the submitted query.

Test both inclusive endpoints and their nearest neighbours

A boundary test is most useful when it distinguishes exact inclusion from “very close.” I06 constructs four float64 coordinates:

nextafter(0, -inf)
0
5
nextafter(5, +inf)

Python documents math.nextafter(x, y) as returning the floating-point value one representable step from x toward y when the default step count is used.

The reference result is consequently sharp: the nearest representable value below 0 is HELD_OUTSIDE; 0 is IN_DOMAIN; 5 is IN_DOMAIN; and the nearest representable value above 5 is HELD_OUTSIDE.

That is the intended numerical contract. No tolerance enlarges [0,5].

The ledger also stores repr(float(q)) and float(q).hex() so a reviewer can inspect the actual floating value supplied to the helper. Those representations are useful numerical evidence, but they do not recover original lexical input. If upstream text said "5.000" rather than "5", or if a NaN carried payload information, this fixture does not preserve that provenance.

Approximate comparison is a separate contract. Refonte Learning's article on NumPy tolerance-reference validation examines which value supplies a comparison allowance; it does not justify adding an undocumented tolerance to this interval-membership test.

If an application genuinely wants a fuzzy admission boundary, it needs an explicit rule stating which tolerance applies, to which endpoint, for what reason, and with dedicated boundary tests. Silent widening would convert a clear interval contract into an implicit model change.

Reject invalid tables before interpolation

The helper does not treat every array accepted by Python syntax as an admissible interpolation table. I08 rejects unsorted knots [0,5,2] and duplicated knots [0,2,2] before its interpolation call.

That policy aligns with the NumPy documentation's warning that, with no period supplied, xp is expected to be increasing and that non-increasing input makes interpolation results meaningless. NumPy even shows np.all(np.diff(xp) > 0) as a simple strict-increase check.

I09 extends admission testing to a NaN in xp, an infinite fp, unequal lengths, two-dimensional xp, integer-typed xp, and a one-knot table. The supplied reference run reports all six as rejected.

Keep the helper's restrictions narrower than the API

Not every helper restriction is a NumPy requirement. This fixture requires ordinary one-dimensional arrays whose dtype is exactly float64; finite xp and fp; at least two knots; equal table lengths; strict ordering; absolute knot and value magnitudes no greater than 100; and unique nonempty string IDs.

Those are authored teaching constraints.

For example, NumPy documents xp as a one-dimensional sequence of floats and permits fp to contain float or complex values, whereas this article deliberately excludes complex interpolation and insists on ordinary float64 ndarrays.

Do not expand the test evidence beyond what was injected. Magnitude violations, an unknown policy value, and every possible malformed ID or container representation were not separately exercised. Likewise, the helper-call counter shows that the tested rejected candidates did not reach the helper's np.interp call. It does not report what an unguarded NumPy invocation would have returned for those malformed cases.

Preserve query identity through every result

A numerical result is not enough if the pipeline cannot prove which submitted row it belongs to.

I07 sends NaN, positive infinity, negative infinity and 1.0 under IDs nan, positive_inf, negative_inf and ok. NumPy's isfinite classifies NaN and either infinity as nonfinite. The helper converts that property into an application state: the first three rows become INVALID_QUERY, while ok becomes IN_DOMAIN with value 12.

Crucially, the invalid rows are retained. Removing them before building the ledger would erase evidence that queries were submitted and prevent downstream reconciliation of input count versus result count.

I10 addresses the inverse problem: two different rows may legitimately contain the same numerical coordinate. IDs r1 and r2, both at coordinate 1, both receive 12. Their equality as numbers does not make them the same request. The helper therefore requires unique query IDs and rejects duplicated IDs or an ID vector whose length does not match the query vector.

I11 then deliberately reorders the main submission:

above, inside_a, below, last, first, inside_b, middle

The reference output remains associated with those IDs and their submitted order. The expected sequence is not reconstructed by sorting coordinates after interpolation. That matters because sorting may be computationally convenient in other algorithms while being unacceptable as a provenance strategy.

A complete main hold ledger can be understood as:

ID

Query

State

Value

below

-1

HELD_OUTSIDE

No value

first

0

IN_DOMAIN

10

inside_a

1

IN_DOMAIN

12

middle

2

IN_DOMAIN

14

inside_b

3.5

IN_DOMAIN

11

last

5

IN_DOMAIN

8

above

6

HELD_OUTSIDE

No value

For each actual JSON record, query_repr and query_hex further preserve the supplied floating value. They do not preserve an earlier CSV token, JSON lexical form, measurement timestamp, unit annotation or source-system identifier. If those forms matter, the calling application must carry them separately.

The guiding rule is simple: coordinate equality is not row identity, and output position should not be asked to substitute for an explicit identity field.

Reconcile the evidence before downstream use

Before any candidate values move downstream, reconcile the result ledger against the submitted request set.

The minimum structural checks are: one result record for every submitted ID; the same submitted order unless a separately documented keyed reordering exists; no duplicate IDs; and exactly one mutually exclusive state for each row. For the main seven-row fixture, state counts must reconcile to five IN_DOMAIN plus two HELD_OUTSIDE under the hold policy, or five IN_DOMAIN plus two DECLARED_CONSTANT_EXTENSION under the constant policy.

Those checks do not replace value validation. The five in-domain values are also compared against the scalar rational oracle.

The oracle is intentionally independent of numpy.interp. For the literal knots, it computes 10 + 2q on [0,2] and 18 - 2q on [2,5] using Fraction. Because the exercised main coordinates and expected results are integers or binary-exact halves, the fixture avoids pretending that agreement here proves arbitrary floating-point rounding behavior. Python's documentation is explicit that converting a float to Fraction preserves that float's exact binary value, not a hypothetical original decimal value.

Keep empty batches and all-held batches visible

I12 supplies both an empty query vector and a two-row batch containing only -1 and 6. Neither causes a helper interpolation call. The all-outside batch still returns two HELD_OUTSIDE records.

That is successful handling, not evidence of data coverage. A batch with zero accepted estimates may be exactly what the admission policy requires.

An adjacent provenance problem appears when tabular alignment introduces new positions or values; Refonte Learning treats that separately in its discussion of provenance for values introduced during alignment. This interpolation ledger does not import pandas reindex or structural-zero policy. It applies the same higher-level discipline (record how a result arose) to a continuous numerical transformation.

Recover using unchanged coordinates and an approved policy

When endpoint-filled outputs have already been produced and their domain meaning is uncertain, recovery should preserve the original question rather than “fixing” the numbers first.

A defensible review sequence is:

  1. Preserve the original table identity, original query IDs and original coordinates. Keep the questionable result as evidence rather than overwriting it.

  2. Reclassify each finite coordinate against the exact numerical domain. Keep nonfinite coordinates as explicit invalid-query records.

  3. Isolate rows whose prior result depended on outside-domain behavior. Do not infer approval merely because the old value equals an endpoint.

  4. Obtain a documented application decision from the authority that owns the model's allowed domain: hold outside queries, or authorize a defined extension rule.

  5. Regenerate a separately identified candidate ledger without changing the original coordinates. If constant extension is approved, label those rows DECLARED_CONSTANT_EXTENSION.

  6. Compare previous and candidate states by ID before replacing any downstream result.

Deleting outside rows is not recovery because it destroys query completeness. Clipping them is not recovery unless clipping itself is the explicitly approved input policy, because NumPy documents that clipping replaces the submitted values with interval edges.

The teaching script does not implement rollback, transactional replacement, publication sinks or failure-atomic recovery. Those are application procedures proposed for a larger system. They should not be reported as outcomes of this laboratory.

The bounded principle is narrower: preserve the coordinate, decide its state, and make any extension policy visible as policy rather than smuggling it into a plausible number.

Assign operational decisions and owners

A numerical pipeline benefits from separating implementation ownership from authority over the table and its model domain.

Evidence state

Operational decision

Primary ownership

Malformed or inadmissible table

Hold pipeline candidate before interpolation

Table/data authority resolves source; pipeline maintainer enforces admission

Nonfinite query

Retain row as INVALID_QUERY; investigate source or calling contract

Query-producing system/data owner

Finite query outside interval

HELD_OUTSIDE, unless a separate extension policy is approved

Model/domain owner approves meaning; maintainer implements it

Finite in-domain query

Retain computed estimate, subject to remaining application checks

Maintainer verifies computation; domain owner still owns scientific applicability

Approved constant extension

Record DECLARED_CONSTANT_EXTENSION, preserving original query

Domain owner authorizes policy; maintainer records and applies it

This ownership model prevents a Python keyword argument from accidentally becoming scientific authority. NumPy defines what left and right do computationally; it cannot determine whether a particular application is permitted to use those values outside its sampled interval.

State what a successful lab leaves unproved

PASS_12_CASES means the supplied fixture's asserted observations and decisions matched its expected inventory in the supplied reference run. It does not establish real calibration validity, uncertainty bounds, sensor certification, arbitrary extrapolation validity, production security, concurrency behavior, deployment reliability, cross-platform qualification or numerical behavior for tables outside the exercised representation.

The same restraint applies to the source hash and environment record. They improve reproducibility and identification; they do not independently authenticate the execution.

Most importantly, IN_DOMAIN remains a numerical interval state. An in-domain interpolated estimate is still a model output, not an observation and not automatic permission for downstream scientific use.

Turn the findings into a release review

A release review for this bounded transformation can be compact because every item points to evidence already exercised rather than to a generic quality slogan.

  • Environment: confirm the intended interpreter and pin NumPy 2.3.5 for reproduction of this fixture. The script itself rejects a NumPy mismatch but only records, rather than enforces, the Python patch.

  • Source identity: retain the reviewed source digest and verify that source arrays remain unchanged after execution.

  • Table admission: require the intended one-dimensional float64 representation, finite values, equal lengths, at least two knots and strict ordering. Record that the magnitude limit is an authored fixture constraint.

  • Query completeness: preserve unique IDs, repeated coordinates with different IDs, submitted order, invalid rows, held rows and empty/all-held batches.

  • Boundary behavior: require exact endpoint inclusion and hold the nearest representable values outside, as I06 demonstrates using nextafter.

  • Independent values: reconcile all admitted main-case estimates with the scalar piecewise-linear oracle.

  • Outside policy: distinguish default endpoint fills, held outside queries, coordinate clipping and declared constant extension rather than comparing only numerical values.

  • Rejected evidence: retain admission failures as well as the accepted branch so review can show what did not reach helper interpolation.

  • Coverage statement: explicitly record untested guards and omitted system properties instead of implying that twelve cases qualify arbitrary replacements.

A changed table, changed extension policy, changed admission rule or changed dependency environment deserves review because it changes an assumption represented by this evidence. That does not mean every such change is wrong; it means this particular result should not be generalized silently.

The release question is consequently not “did NumPy return numbers?” It is whether each submitted ID has an explainable state, whether admitted computations agree with an independent model of the fixture, and whether any outside-domain value has an explicit owner-approved interpretation.

Build deeper NumPy practice with Refonte Learning

The habits exercised here (writing explicit numerical contracts, separating implementation behavior from application policy, preserving row identity, and checking a library result against an independent calculation) fit naturally with broader Python and data-science practice.

Refonte Learning's current Data Science & AI program page lists Python Data Science, statistical modelling, exploratory analysis and visualization, machine learning and predictive modelling, model optimization and problem-solving, and application to industry projects. Its FAQ names Python, Jupyter Notebook, Pandas, NumPy, Matplotlib, scikit-learn and TensorFlow among the tools used.

The visible “Program Specifics” section states three months and 12 to 14 hours per week, while the page also contains broader eligibility and programme descriptions that should be checked against the current terms before enrollment. The page describes practical and virtual-internship elements, but those statements should not be converted into a guarantee of external placement or a particular employment outcome.

Nor does the public page establish that numpy.interp, Fraction, nextafter, domain-admission ledgers or this exact laboratory are taught. NumPy is explicitly named; this specialized workflow is not. Prospective learners should verify the current syllabus, admissions requirements, duration and internship conditions directly.

What the exercise does demonstrate is the kind of problem-solving discipline worth developing around NumPy: do not stop at a successful call. Define what the result is allowed to mean, preserve the evidence needed to review it, and keep an interpolation estimate distinct from both an observation and an explicitly authorized extension.