1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TransformPrecision {
13 F64,
15 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct TransformResources {
38 pub max_scratch_bytes: usize,
40 pub block_len: usize,
42}
43
44impl TransformResources {
45 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#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct TransformReport {
66 pub scratch_bytes: usize,
68 pub passes: usize,
70 pub io_blocks: usize,
72 pub precision: TransformPrecision,
74 pub plan_digest: ContentId,
76}
77
78pub 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 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}