Skip to main content

numcodecs_fourier_network/
lib.rs

1//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io] [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs]
2//!
3//! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/numcodecs-rs/ci.yml?branch=main
4//! [workflow]: https://github.com/juntyr/numcodecs-rs/actions/workflows/ci.yml?query=branch%3Amain
5//!
6//! [MSRV]: https://img.shields.io/badge/MSRV-1.87.0-blue
7//! [repo]: https://github.com/juntyr/numcodecs-rs
8//!
9//! [Latest Version]: https://img.shields.io/crates/v/numcodecs-fourier-network
10//! [crates.io]: https://crates.io/crates/numcodecs-fourier-network
11//!
12//! [Rust Doc Crate]: https://img.shields.io/docsrs/numcodecs-fourier-network
13//! [docs.rs]: https://docs.rs/numcodecs-fourier-network/
14//!
15//! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue
16//! [docs]: https://juntyr.github.io/numcodecs-rs/numcodecs_fourier_network
17//!
18//! Fourier feature neural network codec implementation for the [`numcodecs`] API.
19
20#![allow(clippy::multiple_crate_versions)]
21
22use std::{borrow::Cow, num::NonZeroUsize, ops::AddAssign};
23
24use burn::{
25    backend::{Autodiff, NdArray, ndarray::NdArrayDevice},
26    module::{Module, Param},
27    nn::loss::{MseLoss, Reduction},
28    optim::{AdamConfig, GradientsParams, Optimizer},
29    prelude::Backend,
30    record::{
31        BinBytesRecorder, DoublePrecisionSettings, FullPrecisionSettings, PrecisionSettings,
32        Record, Recorder, RecorderError,
33    },
34    tensor::{
35        Distribution, Element as BurnElement, Float, Tensor, TensorData, backend::AutodiffBackend,
36    },
37};
38use itertools::Itertools;
39use ndarray::{Array, ArrayBase, ArrayView, ArrayViewMut, Data, Dimension, Ix1, Order, Zip};
40use num_traits::{ConstOne, ConstZero, Float as FloatTrait, FromPrimitive};
41use numcodecs::{
42    AnyArray, AnyArrayAssignError, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray,
43    Codec, StaticCodec, StaticCodecConfig, StaticCodecVersion,
44};
45use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
46use serde::{Deserialize, Deserializer, Serialize, Serializer};
47use thiserror::Error;
48
49// FIXME: bytemuck 1.24 fails to compile on 1.87
50use ::bytemuck as _;
51
52// FIXME: burn-common -> cubecl-common brings in wasm-bindgen
53//        wasm-bindgen v0.2.115 has an unresolved import in wasm32-wasi
54use ::wasm_bindgen as _;
55
56#[cfg(test)]
57use ::serde_json as _;
58
59mod modules;
60
61use modules::{Model, ModelConfig, ModelExtra, ModelRecord};
62
63type FourierNetworkCodecVersion = StaticCodecVersion<0, 1, 0>;
64
65#[derive(Clone, Serialize, Deserialize, JsonSchema)]
66#[serde(deny_unknown_fields)]
67/// Fourier network codec which trains and overfits a fourier feature neural
68/// network on encoding and predicts during decoding.
69///
70/// The approach is based on the papers by Tancik et al. 2020
71/// (<https://dl.acm.org/doi/abs/10.5555/3495724.3496356>)
72/// and by Huang and Hoefler 2020 (<https://arxiv.org/abs/2210.12538>).
73pub struct FourierNetworkCodec {
74    /// The number of Fourier features that the data coordinates are projected to
75    pub fourier_features: NonZeroUsize,
76    /// The standard deviation of the Fourier features
77    pub fourier_scale: Positive<f64>,
78    /// The number of blocks in the network
79    pub num_blocks: NonZeroUsize,
80    /// The learning rate for the `Adam` optimizer
81    pub learning_rate: Positive<f64>,
82    /// The number of epochs for which the network is trained
83    pub num_epochs: usize,
84    /// The optional mini-batch size used during training
85    ///
86    /// Setting the mini-batch size to `None` disables the use of batching,
87    /// i.e. the network is trained using one large batch that includes the
88    /// full data.
89    #[serde(deserialize_with = "deserialize_required_option")]
90    #[schemars(required, extend("type" = ["integer", "null"]))]
91    pub mini_batch_size: Option<NonZeroUsize>,
92    /// The seed for the random number generator used during encoding
93    pub seed: u64,
94    /// The codec's encoding format version. Do not provide this parameter explicitly.
95    #[serde(default, rename = "_version")]
96    pub version: FourierNetworkCodecVersion,
97}
98
99// using this wrapper function makes an Option<T> required
100fn deserialize_required_option<'de, T: serde::Deserialize<'de>, D: serde::Deserializer<'de>>(
101    deserializer: D,
102) -> Result<Option<T>, D::Error> {
103    Option::<T>::deserialize(deserializer)
104}
105
106impl Codec for FourierNetworkCodec {
107    type Error = FourierNetworkCodecError;
108
109    fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
110        match data {
111            AnyCowArray::F32(data) => Ok(AnyArray::U8(
112                encode::<f32, _, _, Autodiff<NdArray<f32>>>(
113                    &NdArrayDevice::Cpu,
114                    data,
115                    self.fourier_features,
116                    self.fourier_scale,
117                    self.num_blocks,
118                    self.learning_rate,
119                    self.num_epochs,
120                    self.mini_batch_size,
121                    self.seed,
122                )?
123                .into_dyn(),
124            )),
125            AnyCowArray::F64(data) => Ok(AnyArray::U8(
126                encode::<f64, _, _, Autodiff<NdArray<f64>>>(
127                    &NdArrayDevice::Cpu,
128                    data,
129                    self.fourier_features,
130                    self.fourier_scale,
131                    self.num_blocks,
132                    self.learning_rate,
133                    self.num_epochs,
134                    self.mini_batch_size,
135                    self.seed,
136                )?
137                .into_dyn(),
138            )),
139            encoded => Err(FourierNetworkCodecError::UnsupportedDtype(encoded.dtype())),
140        }
141    }
142
143    fn decode(&self, _encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
144        Err(FourierNetworkCodecError::MissingDecodingOutput)
145    }
146
147    fn decode_into(
148        &self,
149        encoded: AnyArrayView,
150        decoded: AnyArrayViewMut,
151    ) -> Result<(), Self::Error> {
152        let AnyArrayView::U8(encoded) = encoded else {
153            return Err(FourierNetworkCodecError::EncodedDataNotBytes {
154                dtype: encoded.dtype(),
155            });
156        };
157
158        let Ok(encoded): Result<ArrayBase<_, Ix1>, _> = encoded.view().into_dimensionality() else {
159            return Err(FourierNetworkCodecError::EncodedDataNotOneDimensional {
160                shape: encoded.shape().to_vec(),
161            });
162        };
163
164        match decoded {
165            AnyArrayViewMut::F32(decoded) => decode_into::<f32, _, _, NdArray<f32>>(
166                &NdArrayDevice::Cpu,
167                encoded,
168                decoded,
169                self.fourier_features,
170                self.num_blocks,
171            ),
172            AnyArrayViewMut::F64(decoded) => decode_into::<f64, _, _, NdArray<f64>>(
173                &NdArrayDevice::Cpu,
174                encoded,
175                decoded,
176                self.fourier_features,
177                self.num_blocks,
178            ),
179            decoded => Err(FourierNetworkCodecError::UnsupportedDtype(decoded.dtype())),
180        }
181    }
182}
183
184impl StaticCodec for FourierNetworkCodec {
185    const CODEC_ID: &'static str = "fourier-network.rs";
186
187    type Config<'de> = Self;
188
189    fn from_config(config: Self::Config<'_>) -> Self {
190        config
191    }
192
193    fn get_config(&self) -> StaticCodecConfig<'_, Self> {
194        StaticCodecConfig::from(self)
195    }
196}
197
198#[expect(clippy::derive_partial_eq_without_eq)] // floats are not Eq
199#[derive(Copy, Clone, PartialEq, PartialOrd, Hash)]
200/// Positive floating point number
201pub struct Positive<T: FloatTrait>(T);
202
203impl Serialize for Positive<f64> {
204    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205        serializer.serialize_f64(self.0)
206    }
207}
208
209impl<'de> Deserialize<'de> for Positive<f64> {
210    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211        let x = f64::deserialize(deserializer)?;
212
213        if x > 0.0 {
214            Ok(Self(x))
215        } else {
216            Err(serde::de::Error::invalid_value(
217                serde::de::Unexpected::Float(x),
218                &"a positive value",
219            ))
220        }
221    }
222}
223
224impl JsonSchema for Positive<f64> {
225    fn schema_name() -> Cow<'static, str> {
226        Cow::Borrowed("PositiveF64")
227    }
228
229    fn schema_id() -> Cow<'static, str> {
230        Cow::Borrowed(concat!(module_path!(), "::", "Positive<f64>"))
231    }
232
233    fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
234        json_schema!({
235            "type": "number",
236            "exclusiveMinimum": 0.0
237        })
238    }
239}
240
241#[derive(Debug, Error)]
242/// Errors that may occur when applying the [`FourierNetworkCodec`].
243pub enum FourierNetworkCodecError {
244    /// [`FourierNetworkCodec`] does not support the dtype
245    #[error("FourierNetwork does not support the dtype {0}")]
246    UnsupportedDtype(AnyArrayDType),
247    /// [`FourierNetworkCodec`] does not support non-finite (infinite or NaN) floating
248    /// point data
249    #[error("FourierNetwork does not support non-finite (infinite or NaN) floating point data")]
250    NonFiniteData,
251    /// [`FourierNetworkCodec`] failed during a neural network computation
252    #[error("FourierNetwork failed during a neural network computation")]
253    NeuralNetworkError {
254        /// The source of the error
255        #[from]
256        source: NeuralNetworkError,
257    },
258    /// [`FourierNetworkCodec`] must be provided the output array during decoding
259    #[error("FourierNetwork must be provided the output array during decoding")]
260    MissingDecodingOutput,
261    /// [`FourierNetworkCodec`] can only decode one-dimensional byte arrays but
262    /// received an array of a different dtype
263    #[error(
264        "FourierNetwork can only decode one-dimensional byte arrays but received an array of dtype {dtype}"
265    )]
266    EncodedDataNotBytes {
267        /// The unexpected dtype of the encoded array
268        dtype: AnyArrayDType,
269    },
270    /// [`FourierNetworkCodec`] can only decode one-dimensional byte arrays but
271    /// received an array of a different shape
272    #[error(
273        "FourierNetwork can only decode one-dimensional byte arrays but received a byte array of shape {shape:?}"
274    )]
275    EncodedDataNotOneDimensional {
276        /// The unexpected shape of the encoded array
277        shape: Vec<usize>,
278    },
279    /// [`FourierNetworkCodec`] cannot decode into the provided array
280    #[error("FourierNetwork cannot decode into the provided array")]
281    MismatchedDecodeIntoArray {
282        /// The source of the error
283        #[from]
284        source: AnyArrayAssignError,
285    },
286}
287
288#[derive(Debug, Error)]
289#[error(transparent)]
290/// Opaque error for when an error occurs in the neural network
291pub struct NeuralNetworkError(RecorderError);
292
293/// Floating point types.
294pub trait FloatExt:
295    AddAssign + BurnElement + ConstOne + ConstZero + FloatTrait + FromPrimitive
296{
297    /// The precision of this floating point type
298    type Precision: PrecisionSettings;
299
300    /// Convert a usize to a floating point number
301    fn from_usize(x: usize) -> Self;
302}
303
304impl FloatExt for f32 {
305    type Precision = FullPrecisionSettings;
306
307    #[expect(clippy::cast_precision_loss)]
308    fn from_usize(x: usize) -> Self {
309        x as Self
310    }
311}
312
313impl FloatExt for f64 {
314    type Precision = DoublePrecisionSettings;
315
316    #[expect(clippy::cast_precision_loss)]
317    fn from_usize(x: usize) -> Self {
318        x as Self
319    }
320}
321
322#[expect(clippy::similar_names)] // train_xs and train_ys
323#[expect(clippy::missing_panics_doc)] // only panics on implementation bugs
324#[expect(clippy::too_many_arguments)] // FIXME
325/// Encodes the `data` by training a fourier feature neural network.
326///
327/// The `fourier_features` are randomly sampled from a normal distribution with
328/// zero mean and `fourier_scale` standard deviation.
329///
330/// The neural network consists of `num_blocks` blocks.
331///
332/// The network is trained for `num_epochs` using the `learning_rate`
333/// and mini-batches of `mini_batch_size` if mini-batching is enabled.
334///
335/// All random numbers are generated using the provided `seed`.
336///
337/// # Errors
338///
339/// Errors with
340/// - [`FourierNetworkCodecError::NonFiniteData`] if any data element is
341///   non-finite (infinite or NaN)
342/// - [`FourierNetworkCodecError::NeuralNetworkError`] if an error occurs during
343///   the neural network computation
344pub fn encode<T: FloatExt, S: Data<Elem = T>, D: Dimension, B: AutodiffBackend<FloatElem = T>>(
345    device: &B::Device,
346    data: ArrayBase<S, D>,
347    fourier_features: NonZeroUsize,
348    fourier_scale: Positive<f64>,
349    num_blocks: NonZeroUsize,
350    learning_rate: Positive<f64>,
351    num_epochs: usize,
352    mini_batch_size: Option<NonZeroUsize>,
353    seed: u64,
354) -> Result<Array<u8, Ix1>, FourierNetworkCodecError> {
355    let Some(mean) = data.mean() else {
356        return Ok(Array::from_vec(Vec::new()));
357    };
358    let stdv = data.std(T::ZERO);
359    let stdv = if stdv == T::ZERO { T::ONE } else { stdv };
360
361    if !Zip::from(&data).all(|x| x.is_finite()) {
362        return Err(FourierNetworkCodecError::NonFiniteData);
363    }
364
365    B::seed(seed);
366
367    let b_t = Tensor::<B, 2, Float>::random(
368        [data.ndim(), fourier_features.get()],
369        Distribution::Normal(0.0, fourier_scale.0),
370        device,
371    );
372
373    let train_xs = flat_grid_like(&data, device);
374    let train_xs = fourier_mapping(train_xs, b_t.clone());
375
376    let train_ys_shape = [data.len(), 1];
377    let mut train_ys = data.into_owned();
378    train_ys.mapv_inplace(|x| (x - mean) / stdv);
379    #[expect(clippy::unwrap_used)] // reshape with one extra new axis cannot fail
380    let train_ys = train_ys
381        .into_shape_clone((train_ys_shape, Order::RowMajor))
382        .unwrap();
383    let train_ys = Tensor::from_data(
384        TensorData::new(train_ys.into_raw_vec_and_offset().0, train_ys_shape),
385        device,
386    );
387
388    let model = train(
389        device,
390        &train_xs,
391        &train_ys,
392        fourier_features,
393        num_blocks,
394        learning_rate,
395        num_epochs,
396        mini_batch_size,
397        stdv,
398    );
399
400    let extra = ModelExtra {
401        model: model.into_record(),
402        b_t: Param::from_tensor(b_t).set_require_grad(false),
403        mean: Param::from_tensor(Tensor::from_data(
404            TensorData::new(vec![mean], vec![1]),
405            device,
406        ))
407        .set_require_grad(false),
408        stdv: Param::from_tensor(Tensor::from_data(
409            TensorData::new(vec![stdv], vec![1]),
410            device,
411        ))
412        .set_require_grad(false),
413        version: StaticCodecVersion,
414    };
415
416    let recorder = BinBytesRecorder::<T::Precision>::new();
417    let encoded = recorder.record(extra, ()).map_err(NeuralNetworkError)?;
418
419    Ok(Array::from_vec(encoded))
420}
421
422#[expect(clippy::missing_panics_doc)] // only panics on implementation bugs
423/// Decodes the `encoded` data into the `decoded` output array by making a
424/// prediction using the fourier feature neural network.
425///
426/// The network must have been trained during [`encode`] using the same number
427/// of `feature_features` and `num_blocks`.
428///
429/// # Errors
430///
431/// Errors with
432/// - [`FourierNetworkCodecError::MismatchedDecodeIntoArray`] if the encoded
433///   array is empty but the decoded array is not
434/// - [`FourierNetworkCodecError::NeuralNetworkError`] if an error occurs during
435///   the neural network computation
436pub fn decode_into<T: FloatExt, S: Data<Elem = u8>, D: Dimension, B: Backend<FloatElem = T>>(
437    device: &B::Device,
438    encoded: ArrayBase<S, Ix1>,
439    mut decoded: ArrayViewMut<T, D>,
440    fourier_features: NonZeroUsize,
441    num_blocks: NonZeroUsize,
442) -> Result<(), FourierNetworkCodecError> {
443    if encoded.is_empty() {
444        if decoded.is_empty() {
445            return Ok(());
446        }
447
448        return Err(FourierNetworkCodecError::MismatchedDecodeIntoArray {
449            source: AnyArrayAssignError::ShapeMismatch {
450                src: encoded.shape().to_vec(),
451                dst: decoded.shape().to_vec(),
452            },
453        });
454    }
455
456    let encoded = encoded.into_owned().into_raw_vec_and_offset().0;
457
458    let recorder = BinBytesRecorder::<T::Precision>::new();
459    let record: ModelExtra<B> = recorder.load(encoded, device).map_err(NeuralNetworkError)?;
460
461    let model = ModelConfig::new(fourier_features, num_blocks)
462        .init(device)
463        .load_record(record.model);
464    let b_t = record.b_t.into_value();
465    let mean = record.mean.into_value().into_scalar();
466    let stdv = record.stdv.into_value().into_scalar();
467
468    let test_xs = flat_grid_like(&decoded, device);
469    let test_xs = fourier_mapping(test_xs, b_t);
470
471    let prediction = model.forward(test_xs).into_data();
472    #[expect(clippy::unwrap_used)] // same generic type, check must succeed
473    let prediction = prediction.as_slice().unwrap();
474
475    #[expect(clippy::unwrap_used)] // prediction shape is flattened
476    decoded.assign(&ArrayView::from_shape(decoded.shape(), prediction).unwrap());
477    decoded.mapv_inplace(|x| (x * stdv) + mean);
478
479    Ok(())
480}
481
482fn flat_grid_like<T: FloatExt, S: Data<Elem = T>, D: Dimension, B: Backend<FloatElem = T>>(
483    a: &ArrayBase<S, D>,
484    device: &B::Device,
485) -> Tensor<B, 2, Float> {
486    let grid = a
487        .shape()
488        .iter()
489        .copied()
490        .map(|s| {
491            // (0..s).into_iter(), fixed in 1.97
492            #[allow(clippy::useless_conversion)]
493            (0..s)
494                .into_iter()
495                .map(move |x| <T as FloatExt>::from_usize(x) / <T as FloatExt>::from_usize(s))
496        })
497        .multi_cartesian_product()
498        .flatten()
499        .collect::<Vec<_>>();
500
501    Tensor::from_data(TensorData::new(grid, [a.len(), a.ndim()]), device)
502}
503
504fn fourier_mapping<B: Backend>(
505    xs: Tensor<B, 2, Float>,
506    b_t: Tensor<B, 2, Float>,
507) -> Tensor<B, 2, Float> {
508    let xs_proj = xs.mul_scalar(core::f64::consts::TAU).matmul(b_t);
509
510    Tensor::cat(vec![xs_proj.clone().sin(), xs_proj.cos()], 1)
511}
512
513#[expect(clippy::similar_names)] // train_xs and train_ys
514#[expect(clippy::too_many_arguments)] // FIXME
515fn train<T: FloatExt, B: AutodiffBackend<FloatElem = T>>(
516    device: &B::Device,
517    train_xs: &Tensor<B, 2, Float>,
518    train_ys: &Tensor<B, 2, Float>,
519    fourier_features: NonZeroUsize,
520    num_blocks: NonZeroUsize,
521    learning_rate: Positive<f64>,
522    num_epochs: usize,
523    mini_batch_size: Option<NonZeroUsize>,
524    stdv: T,
525) -> Model<B> {
526    let num_samples = train_ys.shape().num_elements();
527    let num_batches = mini_batch_size.map(|b| num_samples.div_ceil(b.get()));
528
529    let mut model = ModelConfig::new(fourier_features, num_blocks).init(device);
530    let mut optim = AdamConfig::new().init();
531
532    let mut best_loss = T::infinity();
533    let mut best_epoch = 0;
534    let mut best_model_checkpoint = model.clone().into_record().into_item::<T::Precision>();
535
536    for epoch in 1..=num_epochs {
537        #[expect(clippy::option_if_let_else)]
538        let (train_xs_batches, train_ys_batches) = match num_batches {
539            Some(num_batches) => {
540                let shuffle = Tensor::<B, 1, Float>::random(
541                    [num_samples],
542                    Distribution::Uniform(0.0, 1.0),
543                    device,
544                );
545                let shuffle_indices = shuffle.argsort(0);
546
547                let train_xs_shuffled = train_xs.clone().select(0, shuffle_indices.clone());
548                let train_ys_shuffled = train_ys.clone().select(0, shuffle_indices);
549
550                (
551                    train_xs_shuffled.chunk(num_batches, 0),
552                    train_ys_shuffled.chunk(num_batches, 0),
553                )
554            }
555            None => (vec![train_xs.clone()], vec![train_ys.clone()]),
556        };
557
558        let mut loss_sum = T::ZERO;
559
560        let mut se_sum = T::ZERO;
561        let mut ae_sum = T::ZERO;
562        let mut l_inf = T::ZERO;
563
564        for (train_xs_batch, train_ys_batch) in train_xs_batches.into_iter().zip(train_ys_batches) {
565            let prediction = model.forward(train_xs_batch);
566            let loss =
567                MseLoss::new().forward(prediction.clone(), train_ys_batch.clone(), Reduction::Mean);
568
569            let grads = GradientsParams::from_grads(loss.backward(), &model);
570            model = optim.step(learning_rate.0, model, grads);
571
572            loss_sum += loss.into_scalar();
573
574            let err = prediction - train_ys_batch;
575
576            se_sum += (err.clone() * err.clone()).sum().into_scalar();
577            ae_sum += err.clone().abs().sum().into_scalar();
578            l_inf = l_inf.max(err.abs().max().into_scalar());
579        }
580
581        let loss_mean = loss_sum / <T as FloatExt>::from_usize(num_batches.unwrap_or(1));
582
583        if loss_mean < best_loss {
584            best_loss = loss_mean;
585            best_epoch = epoch;
586            best_model_checkpoint = model.clone().into_record().into_item::<T::Precision>();
587        }
588
589        let rmse = stdv * (se_sum / <T as FloatExt>::from_usize(num_samples)).sqrt();
590        let mae = stdv * ae_sum / <T as FloatExt>::from_usize(num_samples);
591        let l_inf = stdv * l_inf;
592
593        log::info!(
594            "[{epoch}/{num_epochs}]: loss={loss_mean:0.3} MAE={mae:0.3} RMSE={rmse:0.3} Linf={l_inf:0.3}"
595        );
596    }
597
598    if best_epoch != num_epochs {
599        model = model.load_record(ModelRecord::from_item(best_model_checkpoint, device));
600
601        log::info!("restored from epoch {best_epoch} with lowest loss={best_loss:0.3}");
602    }
603
604    model
605}
606
607#[cfg(test)]
608#[expect(clippy::unwrap_used)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn empty() {
614        std::mem::drop(simple_logger::init());
615
616        let encoded = encode::<f32, _, _, Autodiff<NdArray<f32>>>(
617            &NdArrayDevice::Cpu,
618            Array::<f32, _>::zeros((0,)),
619            NonZeroUsize::MIN,
620            Positive(1.0),
621            NonZeroUsize::MIN,
622            Positive(1e-4),
623            10,
624            None,
625            42,
626        )
627        .unwrap();
628        assert!(encoded.is_empty());
629        let mut decoded = Array::<f32, _>::zeros((0,));
630        decode_into::<f32, _, _, NdArray<f32>>(
631            &NdArrayDevice::Cpu,
632            encoded,
633            decoded.view_mut(),
634            NonZeroUsize::MIN,
635            NonZeroUsize::MIN,
636        )
637        .unwrap();
638    }
639
640    #[test]
641    fn ones() {
642        std::mem::drop(simple_logger::init());
643
644        let encoded = encode::<f32, _, _, Autodiff<NdArray<f32>>>(
645            &NdArrayDevice::Cpu,
646            Array::<f32, _>::zeros((1, 1, 1, 1)),
647            NonZeroUsize::MIN,
648            Positive(1.0),
649            NonZeroUsize::MIN,
650            Positive(1e-4),
651            10,
652            None,
653            42,
654        )
655        .unwrap();
656        let mut decoded = Array::<f32, _>::zeros((1, 1, 1, 1));
657        decode_into::<f32, _, _, NdArray<f32>>(
658            &NdArrayDevice::Cpu,
659            encoded,
660            decoded.view_mut(),
661            NonZeroUsize::MIN,
662            NonZeroUsize::MIN,
663        )
664        .unwrap();
665    }
666
667    #[test]
668    fn r#const() {
669        std::mem::drop(simple_logger::init());
670
671        let encoded = encode::<f32, _, _, Autodiff<NdArray<f32>>>(
672            &NdArrayDevice::Cpu,
673            Array::<f32, _>::from_elem((2, 1, 3), 42.0),
674            NonZeroUsize::MIN,
675            Positive(1.0),
676            NonZeroUsize::MIN,
677            Positive(1e-4),
678            10,
679            None,
680            42,
681        )
682        .unwrap();
683        let mut decoded = Array::<f32, _>::zeros((2, 1, 3));
684        decode_into::<f32, _, _, NdArray<f32>>(
685            &NdArrayDevice::Cpu,
686            encoded,
687            decoded.view_mut(),
688            NonZeroUsize::MIN,
689            NonZeroUsize::MIN,
690        )
691        .unwrap();
692    }
693
694    #[test]
695    fn const_batched() {
696        std::mem::drop(simple_logger::init());
697
698        let encoded = encode::<f32, _, _, Autodiff<NdArray<f32>>>(
699            &NdArrayDevice::Cpu,
700            Array::<f32, _>::from_elem((2, 1, 3), 42.0),
701            NonZeroUsize::MIN,
702            Positive(1.0),
703            NonZeroUsize::MIN,
704            Positive(1e-4),
705            10,
706            Some(NonZeroUsize::MIN.saturating_add(1)),
707            42,
708        )
709        .unwrap();
710        let mut decoded = Array::<f32, _>::zeros((2, 1, 3));
711        decode_into::<f32, _, _, NdArray<f32>>(
712            &NdArrayDevice::Cpu,
713            encoded,
714            decoded.view_mut(),
715            NonZeroUsize::MIN,
716            NonZeroUsize::MIN,
717        )
718        .unwrap();
719    }
720
721    #[test]
722    fn linspace() {
723        std::mem::drop(simple_logger::init());
724
725        let data = Array::linspace(0.0_f64, 100.0_f64, 100);
726
727        let fourier_features = NonZeroUsize::new(16).unwrap();
728        let fourier_scale = Positive(10.0);
729        let num_blocks = NonZeroUsize::new(2).unwrap();
730        let learning_rate = Positive(1e-4);
731        let num_epochs = 100;
732        let seed = 42;
733
734        for mini_batch_size in [
735            None,                                         // no mini-batching
736            Some(NonZeroUsize::MIN),                      // stochastic
737            Some(NonZeroUsize::MIN.saturating_add(6)),    // mini-batched, remainder
738            Some(NonZeroUsize::MIN.saturating_add(9)),    // mini-batched
739            Some(NonZeroUsize::MIN.saturating_add(1000)), // mini-batched, truncated
740        ] {
741            let mut decoded = Array::<f64, _>::zeros(data.shape());
742            let encoded = encode::<f64, _, _, Autodiff<NdArray<f64>>>(
743                &NdArrayDevice::Cpu,
744                data.view(),
745                fourier_features,
746                fourier_scale,
747                num_blocks,
748                learning_rate,
749                num_epochs,
750                mini_batch_size,
751                seed,
752            )
753            .unwrap();
754
755            decode_into::<f64, _, _, NdArray<f64>>(
756                &NdArrayDevice::Cpu,
757                encoded,
758                decoded.view_mut(),
759                fourier_features,
760                num_blocks,
761            )
762            .unwrap();
763        }
764    }
765}