Parameters & Errors¶
Per-row parameters¶
Most operation parameters accept a Polars expression as well as a literal. An expression is evaluated against the DataFrame and resolved per row when the pipeline runs:
import polars as pl
from polars_cv import Pipeline
# Literal: the same for every row
pipe = Pipeline().source("image_bytes").resize(height=224, width=224)
# Per row, from other columns
pipe = Pipeline().source("image_bytes").resize(
height=pl.col("target_h"), width=pl.col("target_w")
)
# An aggregation is one value, broadcast to every row
pipe = Pipeline().source("image_bytes").crop(
top=0, left=0, height=pl.col("crop_h").min(), width=pl.col("crop_w").min()
)
Which parameters can be expressions¶
The rule is not a parameter's type: a parameter may be per-row if and only if its value cannot change the output's rank, dtype or domain. The lazy schema is computed when the query is planned and must match what executes.
- Sizes may be per-row.
resize(height=pl.col("h"))is fine; that axis is then unknown at plan time, which the schema says (aListlevel rather than anArrayone). Anarraysink needs a known shape, so pair a per-row size with thelistornumpysink, or follow it with a fixed-size op. - Non-structural enums and flags may be per-row: a resize
filter,interpolation,pad(mode=),convolve2d(border=, normalize=), a morphologyfootprint,distance_transform(signed=), and so on. - Structural parameters are literal-only:
cast(dtype=),normalize(method=, out_dtype=), a reductionaxis,perceptual_hash(hash_size=, algorithm=),rotate(expand=),histogram(closed=, output=), thetranspose/flipaxis lists andapply_lut(out_dtype=). Passing an expression raisesTypeErrorwhen the pipeline is built. - For a list-valued parameter, the length is structural and the elements are
not. A
convolve2dkernel, anormalizemean/std, achannel_swaporder, an affinematrix,reshape's sizes and anapply_luttable keep a literal number of entries, but any entry may be an expression.
Each method's signature in the API reference says
which applies: a parameter typed IntOrExpr, FloatOrExpr, BoolOrExpr or ... | pl.Expr
accepts an expression, and one typed int, str or list[int] alone does
not.
histogram(bins=...) is the one per-row parameter that fixes a length: a
per-row bins cannot feed an array sink, which needs that length when the
query is planned. Use the list sink, or a literal bins.
A per-row value is checked when its row runs. A literal is checked when the
pipeline is built: blur(sigma=-1) raises at once, naming the op, while
blur(sigma=pl.col("s")) with a negative s fails that row (see
Errors for what a failing row does).
Null parameter values¶
A parameter column can contain nulls. By default a null parameter fails the query, naming the column and the row:
.on_null_param("null") opts into null-in/null-out instead: the outputs that
depend on the affected operation are null for that row, exactly as for a null
input image, and every other row and output proceeds.
pipe = (
Pipeline()
.source("image_bytes")
.resize(height=pl.col("h"), width=pl.col("w"))
.on_null_param("null")
)
This is deliberately separate from .on_error(): under "null" a null
parameter is not an error at all, so decode, encode and genuine operation
failures still raise. You do not have to hide real bugs to tolerate missing
parameter values.
For a fallback value rather than a null, fill the null in the expression:
pl.col("h").fill_null(224). There is no policy for this because Polars
already expresses it.
An aggregation broadcasts, so a null in a length-1 parameter (for example
pl.col("h").max() over an all-null column) nulls every row.
.on_null_param() is a graph-level setting, mirrored on LazyPipelineExpr
so you can set it after .pipe(...). When pipelines are composed, a non-default
policy wins: composing a "null" pipeline with a "raise" one gives the whole
graph "null". The .contour / .point / .bbox namespaces carry the same
policy on the accessor:
pl.col("c").contour.on_null("null").normalize(pl.col("w"), 100).
Errors¶
By default, a failure while producing a row raises and aborts the query, with the earliest failing row's error. Two controls tolerate per-row failures, from narrowest to broadest.
Source-level: source(..., on_error="null") covers decode failures only.
A row that fails to decode is null in the outputs that depend on that source;
the rest of the DataFrame still processes.
# Skip undecodable images instead of aborting
pipe = Pipeline().source("image_bytes", on_error="null").resize(height=224, width=224)
Graph-level: .on_error(policy) covers any error that produces a row:
source decode, operation execution or output encoding.
# "raise" (default): the first failing row fails the expression with its error.
pipe = Pipeline().source("image_bytes").resize(height=224, width=224)
# "null": failing rows are null in ALL of the graph's outputs; good rows are unaffected.
pipe = Pipeline().source("image_bytes").resize(height=224, width=224).on_error("null")
# "null_with_message": like "null", but the output becomes a struct with a
# reserved `_error` field holding the failure message (null for good rows).
# A single-output pipeline becomes a two-field struct (`_output` + `_error`).
pipe = (
Pipeline()
.source("image_bytes")
.resize(height=224, width=224)
.on_error("null_with_message")
)
.on_error() is mirrored on LazyPipelineExpr. When pipelines are composed
(by merge_pipe or a binary op) they must agree on it.
Some inputs fail the query under every policy, because they are a mistake in
the query rather than in a row: a column of the wrong type (a source("contour")
over a column that is not contours, say), or a literal parameter no row could
use.