#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
#19's multi-channel
interpolate_intoalways evaluates every channel. A caller wantingone 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:
Plus the batch equivalent, and a matching method on
Interp2DMultiandDynInterpolatorMulti. Out-of-range indices inchannelsare 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 theshared-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:
Interp2DandInterp2DMultiInterp2DMultionlyNeither 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 anInterpData2DViewedthat a plainStrategy2Dcan interpolate against, givingsingle-channel cost with single-copy storage. Two caveats keep it from closing this
issue:
values.index_axis(Axis(2), k)over achannel-last layout has stride
n_channels). Any strategy reaching fordata.values.as_slice().unwrap()panics on it. That is not hypothetical: several ofQCDLab/neopdf's strategies do exactly that (LogChebyshevInterpolation'sStrategy1D/2D/3D/NDimpls,AlphaSCubicInterpolation).Downstream motivation
QCDLab/neopdf's all-flavor evaluators takepid_slots: &[Option<usize>], an arbitrarysubset and permutation of channels, with
Nonemeaning "requested flavor absent fromthis set, yield 0.0". The subset and permutation half is what this issue covers. The
Nonesentinel is PDF-domain logic and should stay in neopdf: it maps cleanly ontopartitioning 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) threadpid_slotsthrough, andneopdf_pdf_xfxq2_allpidsexposes it across the C ABI.Dependencies
Strategy*Multiandchannel_view. Must land first.*_intointerpolation variants #45: the_intoconvention these signatures follow.