Skip to content

Channel subsetting for Strategy*Multi #47

Description

@kylecarow

#19's multi-channel interpolate_into always evaluates every channel. A caller wanting
one channel out of many (the gluon from a 13-flavor PDF set, the green channel of an RGB
image) pays for all of them. Add a way to request a subset.

API changes

Additive, defaulted, so it does not disturb #19's required-method story. 2D shown:

/// Interpolate only the channels listed in `channels`, writing channel
/// `channels[i]` into `out[i]`. `out.len()` must equal `channels.len()`.
///
/// Default evaluates all channels into a scratch buffer and gathers. Override
/// when the strategy can skip the unrequested channels outright, which is every
/// strategy whose per-channel work is a lookup or blend against a shared locate
/// result, i.e. all of them in this crate.
fn interpolate_channels_into(
    &self,
    data: &InterpData2DMulti<D>,
    point: &[D::Elem; 2],
    channels: &[usize],
    out: &mut [D::Elem],
) -> Result<(), InterpolateError> { /* default: evaluate all, gather */ }

Plus the batch equivalent, and a matching method on Interp2DMulti and
DynInterpolatorMulti. Out-of-range indices in channels are an error
(InterpolateError::Other, or a dedicated variant), not silently skipped.

Why not in #19

Purely additive on top of #19's trait, since the default body is expressible in terms of
the required interpolate_into. Splitting it keeps #19's reviewable surface to the
shared-grid mechanism itself.

But it should not drift far behind #19, because of an interaction worth stating
explicitly.

The duplication it resolves

Without subsetting, a consumer that has both single-channel and all-channel access
patterns has to choose:

Keep per-channel Interp2D and Interp2DMulti Interp2DMulti only
All-channel call One locate, all channels One locate, all channels
Single-channel call One locate, one channel One locate, all channels, discard n-1
Grid axis storage n_channels + 1 copies 1 copy
Values storage 2 copies 1 copy

Neither column is acceptable, and #19's memory win only materializes in the right-hand
one. Subsetting makes the right-hand column strictly better than the left on both axes.

#19's InterpData2DMulti::channel_view(k) is a partial escape hatch: it hands back an
InterpData2DViewed that a plain Strategy2D can interpolate against, giving
single-channel cost with single-copy storage. Two caveats keep it from closing this
issue:

  • The view is strided, not contiguous (values.index_axis(Axis(2), k) over a
    channel-last layout has stride n_channels). Any strategy reaching for
    data.values.as_slice().unwrap() panics on it. That is not hypothetical: several of
    QCDLab/neopdf's strategies do exactly that (LogChebyshevInterpolation's
    Strategy1D/2D/3D/ND impls, AlphaSCubicInterpolation).
  • It only covers exactly one channel, not an arbitrary subset or reordering.

Downstream motivation

QCDLab/neopdf's all-flavor evaluators take pid_slots: &[Option<usize>], an arbitrary
subset and permutation of channels, with None meaning "requested flavor absent from
this set, yield 0.0". The subset and permutation half is what this issue covers. The
None sentinel is PDF-domain logic and should stay in neopdf: it maps cleanly onto
partitioning the request before the call.

Both of its evaluation paths (InterleavedHermite::eval_allpids,
ChebyshevAllPids::eval_allpids) and both of its public entry points
(GridPDF::xfxq2_allpids, xfxq2_allpids_with_slots) thread pid_slots through, and
neopdf_pdf_xfxq2_allpids exposes it across the C ABI.

Dependencies

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions