Skip to main content

sim_lib_numbers_signal/
io_plan.rs

1//! Inspectable resource and provenance records for tensor transforms.
2
3use sim_kernel::{ContentId, Datum, Symbol};
4
5use crate::{
6    Direction, Normalization, PaddingPolicy, PlacementPolicy, SignConvention, SignalError,
7    SpectrumPacking, TransformKind, TransformPlan, tensor_view::transform_cell_count,
8};
9
10/// Cell representation used by a multidimensional transform.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TransformPrecision {
13    /// IEEE-754 binary64 real cells.
14    F64,
15    /// Pairs of IEEE-754 binary64 real and imaginary components.
16    ComplexF64,
17}
18
19impl TransformPrecision {
20    pub(crate) const fn cell_bytes(self) -> usize {
21        match self {
22            Self::F64 => size_of::<f64>(),
23            Self::ComplexF64 => 2 * size_of::<f64>(),
24        }
25    }
26
27    fn name(self) -> &'static str {
28        match self {
29            Self::F64 => "f64",
30            Self::ComplexF64 => "complex-f64",
31        }
32    }
33}
34
35/// Caller-declared memory budget and external block granularity.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct TransformResources {
38    /// Hard ceiling for peak transform scratch storage.
39    pub max_scratch_bytes: usize,
40    /// Maximum number of tensor cells in one external block.
41    pub block_len: usize,
42}
43
44impl TransformResources {
45    /// Validates that both limits are nonzero.
46    pub fn validate(self) -> Result<(), SignalError> {
47        if self.max_scratch_bytes == 0 {
48            return Err(SignalError::InvalidPolicy {
49                policy: "max_scratch_bytes",
50                reason: "scratch limit must be nonzero",
51            });
52        }
53        if self.block_len == 0 {
54            return Err(SignalError::InvalidPolicy {
55                policy: "block_len",
56                reason: "external block length must be nonzero",
57            });
58        }
59        Ok(())
60    }
61}
62
63/// Auditable execution facts for an in-memory or blocked transform.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct TransformReport {
66    /// Peak temporary storage required by the plan, excluding result storage.
67    pub scratch_bytes: usize,
68    /// Number of complete separable axis passes.
69    pub passes: usize,
70    /// Total Table/Dir blocks read or written.
71    pub io_blocks: usize,
72    /// Cell representation used for execution.
73    pub precision: TransformPrecision,
74    /// Content digest of shape, axes, conventions, and optional resource plan.
75    pub plan_digest: ContentId,
76}
77
78/// Computes the stable content digest for a multidimensional transform plan.
79pub fn transform_plan_digest(
80    shape: &[usize],
81    axes: &[usize],
82    plan: &TransformPlan,
83    precision: TransformPrecision,
84    resources: Option<TransformResources>,
85) -> ContentId {
86    let resources = match resources {
87        Some(resources) => Datum::Node {
88            tag: Symbol::qualified("numbers-signal", "resources-v1"),
89            fields: vec![
90                datum_field(
91                    "max-scratch-bytes",
92                    Datum::String(resources.max_scratch_bytes.to_string()),
93                ),
94                datum_field("block-len", Datum::String(resources.block_len.to_string())),
95            ],
96        },
97        None => Datum::Nil,
98    };
99    Datum::Node {
100        tag: Symbol::qualified("numbers-signal", "transform-plan-v1"),
101        fields: vec![
102            datum_field("shape", usize_vector(shape)),
103            datum_field("axes", usize_vector(axes)),
104            datum_field("kind", Datum::String(kind_name(plan.kind))),
105            datum_field("template-len", Datum::String(plan.len.to_string())),
106            datum_field(
107                "direction",
108                Datum::String(direction_name(plan.direction).into()),
109            ),
110            datum_field(
111                "normalization",
112                Datum::String(normalization_name(plan.normalization).into()),
113            ),
114            datum_field("sign", Datum::String(sign_name(plan.sign).into())),
115            datum_field("packing", Datum::String(packing_name(plan.packing).into())),
116            datum_field("length", Datum::String(format!("{:?}", plan.length))),
117            datum_field("padding", Datum::String(padding_name(plan.padding).into())),
118            datum_field(
119                "placement",
120                Datum::String(placement_name(plan.placement).into()),
121            ),
122            datum_field("precision", Datum::String(precision.name().into())),
123            datum_field("resources", resources),
124        ],
125    }
126    .content_id()
127    .expect("transform plan datum has unique named fields")
128}
129
130pub(crate) fn scratch_bytes(
131    shape: &[usize],
132    axes: &[usize],
133    precision: TransformPrecision,
134    block_len: usize,
135) -> Result<usize, SignalError> {
136    let _ = transform_cell_count(shape)?;
137    let max_line =
138        axes.iter()
139            .map(|&axis| shape[axis])
140            .max()
141            .ok_or(SignalError::InvalidPolicy {
142                policy: "axes",
143                reason: "at least one transform axis is required",
144            })?;
145    // The executor holds one caller line plus the 1-D transform's selected
146    // input, output, mixed-radix children, or two Bluestein convolution
147    // buffers. Sixteen cell widths is a conservative peak for every path.
148    let line = max_line
149        .checked_mul(precision.cell_bytes())
150        .and_then(|bytes| bytes.checked_mul(16))
151        .ok_or(SignalError::InvalidTensorView {
152            reason: "transform scratch size overflowed",
153        })?;
154    let block =
155        block_len
156            .checked_mul(precision.cell_bytes())
157            .ok_or(SignalError::InvalidTensorView {
158                reason: "block scratch size overflowed",
159            })?;
160    line.checked_add(block)
161        .ok_or(SignalError::InvalidTensorView {
162            reason: "transform scratch size overflowed",
163        })
164}
165
166fn datum_field(name: &str, value: Datum) -> (Symbol, Datum) {
167    (Symbol::new(name), value)
168}
169
170fn usize_vector(values: &[usize]) -> Datum {
171    Datum::Vector(
172        values
173            .iter()
174            .map(|value| Datum::String(value.to_string()))
175            .collect(),
176    )
177}
178
179fn kind_name(kind: TransformKind) -> String {
180    match kind {
181        TransformKind::Dft => "dft".into(),
182        TransformKind::Fft => "fft".into(),
183        TransformKind::RealFft => "real-fft".into(),
184        TransformKind::Dct(kind) => format!("dct-{kind:?}"),
185        TransformKind::Dst(kind) => format!("dst-{kind:?}"),
186    }
187}
188
189fn direction_name(direction: Direction) -> &'static str {
190    match direction {
191        Direction::Forward => "forward",
192        Direction::Inverse => "inverse",
193    }
194}
195
196fn normalization_name(normalization: Normalization) -> &'static str {
197    match normalization {
198        Normalization::None => "none",
199        Normalization::Forward => "forward",
200        Normalization::Inverse => "inverse",
201        Normalization::Orthonormal => "orthonormal",
202    }
203}
204
205fn sign_name(sign: SignConvention) -> &'static str {
206    match sign {
207        SignConvention::NegativeForward => "negative-forward",
208        SignConvention::PositiveForward => "positive-forward",
209    }
210}
211
212fn packing_name(packing: SpectrumPacking) -> &'static str {
213    match packing {
214        SpectrumPacking::Full => "full",
215        SpectrumPacking::HermitianHalf => "hermitian-half",
216    }
217}
218
219fn padding_name(padding: PaddingPolicy) -> &'static str {
220    match padding {
221        PaddingPolicy::Reject => "reject",
222        PaddingPolicy::Zero => "zero",
223    }
224}
225
226fn placement_name(placement: PlacementPolicy) -> &'static str {
227    match placement {
228        PlacementPolicy::OutOfPlace => "out-of-place",
229        PlacementPolicy::InPlace => "in-place",
230    }
231}