Skip to main content

aeon_tk/kernel/
node.rs

1//! Module for working with uniform rectangular grids of nodes.
2//!
3//! This module introduces the primary abstraction of `aeon_kernel`, namely
4//! the `NodeSpace`.
5
6use crate::geometry::{
7    CartesianIter, Face, FaceArray, HyperBox, IndexSpace, Region, Side, regions,
8};
9use crate::kernel::{
10    Border, BoundaryClass, BoundaryConds, BoundaryKind, Convolution, Interpolant, Kernel, Value,
11    boundary::is_boundary_compatible,
12};
13use std::array::{self, from_fn};
14
15#[derive(Copy, Clone, PartialEq, Eq, Debug)]
16pub enum Support {
17    Interior,
18    Negative(usize),
19    Positive(usize),
20}
21
22/// Converts a (signed) node index into an (unsigned) vertex index.
23pub fn vertex_from_node<const N: usize>(node: [isize; N]) -> [usize; N] {
24    array::from_fn(|axis| node[axis] as usize)
25}
26
27/// Converts a (unsigned) vertex index into an (signed) node index.
28pub fn node_from_vertex<const N: usize>(vertex: [usize; N]) -> [isize; N] {
29    array::from_fn(|axis| vertex[axis] as isize)
30}
31
32/// A uniform rectangular domain of nodes to which various derivative and interpolations can be applied.
33#[derive(Debug, Clone)]
34pub struct NodeSpace<const N: usize> {
35    /// Number of cells along each axis (one less than then number of vertices).
36    pub size: [usize; N],
37    /// Number of ghost nodes along each face.
38    pub ghost: usize,
39    /// Position of the node space.
40    pub bounds: HyperBox<N>,
41    pub boundary: FaceArray<N, BoundaryClass>,
42}
43
44impl<const N: usize> NodeSpace<N> {
45    pub fn ghost(&self) -> usize {
46        self.ghost
47    }
48
49    pub fn cell_size(&self) -> [usize; N] {
50        self.size
51    }
52
53    pub fn vertex_size(&self) -> [usize; N] {
54        array::from_fn(|axis| self.size[axis] + 1)
55    }
56
57    pub fn node_size(&self) -> [usize; N] {
58        array::from_fn(|axis| self.size[axis] + 1 + 2 * self.ghost)
59    }
60
61    /// The total number of nodes in the `Node` Space.
62    pub fn num_nodes(&self) -> usize {
63        self.node_size().iter().product()
64    }
65
66    /// Returns the spacing along each axis of the node space.
67    pub fn spacing(&self) -> [f64; N] {
68        from_fn(|axis| self.bounds.size[axis] / self.size[axis] as f64)
69    }
70
71    pub fn spacing_axis(&self, axis: usize) -> f64 {
72        self.bounds.size[axis] / self.size[axis] as f64
73    }
74
75    /// Returns true if the node lies inside the interior of the nodespace (i.e. it is not a  ghost node).
76    pub fn is_interior(&self, node: [isize; N]) -> bool {
77        (0..N)
78            .map(|axis| node[axis] >= 0 && node[axis] <= self.size[axis] as isize)
79            .all(|b| b)
80    }
81
82    /// Computes the position of the given node.
83    pub fn position(&self, node: [isize; N]) -> [f64; N] {
84        let spacing: [_; N] = from_fn(|axis| self.bounds.size[axis] / self.size[axis] as f64);
85
86        let mut result = [0.0; N];
87
88        for i in 0..N {
89            result[i] = self.bounds.origin[i] + spacing[i] * node[i] as f64;
90        }
91
92        result
93    }
94
95    /// Computes a linear index from a nodal index.
96    pub fn index_from_node(&self, node: [isize; N]) -> usize {
97        for axis in 0..N {
98            debug_assert!(node[axis] >= -(self.ghost as isize));
99            debug_assert!(node[axis] <= (self.size[axis] + self.ghost) as isize);
100        }
101
102        let cartesian = array::from_fn(|axis| {
103            let mut vertex = node[axis];
104            vertex += self.ghost as isize;
105            vertex as usize
106        });
107
108        IndexSpace::new(self.node_size()).linear_from_cartesian(cartesian)
109    }
110
111    /// Computes a linear index from a vertex index.
112    pub fn index_from_vertex(&self, vertex: [usize; N]) -> usize {
113        self.index_from_node(node_from_vertex(vertex))
114    }
115
116    /// Returns a window of just the interior and edges of the node space (no ghost nodes).
117    pub fn inner_window(&self) -> NodeWindow<N> {
118        NodeWindow {
119            origin: [0isize; N],
120            size: self.vertex_size(),
121        }
122    }
123
124    /// Returns the window which encompasses the whole node space
125    pub fn full_window(&self) -> NodeWindow<N> {
126        let mut origin = [0; N];
127
128        for axis in 0..N {
129            origin[axis] = -(self.ghost as isize);
130        }
131
132        NodeWindow {
133            origin,
134            size: self.node_size(),
135        }
136    }
137
138    /// Returns a window over a region of the node space.
139    pub fn region_window(&self, extent: usize, region: Region<N>) -> NodeWindow<N> {
140        debug_assert!(extent <= self.ghost);
141
142        let origin = array::from_fn(|axis| match region.side(axis) {
143            Side::Left => -(extent as isize),
144            Side::Right => self.size[axis] as isize + 1,
145            Side::Middle => 0,
146        });
147
148        let size = array::from_fn(|axis: usize| match region.side(axis) {
149            Side::Left | Side::Right => extent,
150            Side::Middle => self.size[axis] + 1,
151        });
152
153        NodeWindow { origin, size }
154    }
155
156    /// Returns a window over all nodes on a given face.
157    pub fn face_window(&self, Face { axis, side }: Face<N>) -> NodeWindow<N> {
158        let intercept = if side { self.size[axis] } else { 0 } as isize;
159
160        let mut origin = [0; N];
161        let mut size = array::from_fn(|axis| self.size[axis] + 1);
162
163        origin[axis] = intercept;
164        size[axis] = 1;
165
166        NodeWindow { origin, size }
167    }
168
169    pub fn face_window_disjoint(&self, face: Face<N>) -> NodeWindow<N> {
170        let intercept = if face.side { self.size[face.axis] } else { 0 } as isize;
171
172        let mut origin = [0; N];
173        let mut size = self.vertex_size();
174
175        origin[face.axis] = intercept;
176        size[face.axis] = 1;
177
178        if face.side {
179            for axis in (0..N).filter(|&axis| axis != face.axis) {
180                origin[axis] += 1;
181                size[axis] -= 1;
182            }
183
184            for axis in 0..face.axis {
185                size[axis] -= 1;
186            }
187        } else {
188            for axis in 0..face.axis {
189                origin[axis] += 1;
190                size[axis] -= 1;
191            }
192        }
193
194        NodeWindow { origin, size }
195    }
196
197    pub fn contains_window(&self, window: NodeWindow<N>) -> bool {
198        (0..N).into_iter().all(|axis| {
199            window.origin[axis] >= -(self.ghost as isize)
200                && window.size[axis] < self.size[axis] + 1 + 2 * self.ghost
201        })
202    }
203
204    fn support_axis(&self, vertex: usize, border_width: usize, axis: usize) -> Support {
205        debug_assert!(self.ghost >= border_width);
206        debug_assert!(self.size[axis] + 1 >= border_width);
207
208        let has_negative = self.boundary[Face::negative(axis)].has_ghost();
209        let has_positive = self.boundary[Face::positive(axis)].has_ghost();
210
211        if !has_negative && vertex < border_width {
212            Support::Negative(vertex as usize)
213        } else if !has_positive && vertex >= (self.size[axis] + 1 - border_width) {
214            Support::Positive(self.size[axis] - vertex as usize)
215        } else {
216            Support::Interior
217        }
218    }
219
220    fn support_cell_axis(&self, cell: usize, border_width: usize, axis: usize) -> Support {
221        debug_assert!(self.ghost >= border_width);
222        debug_assert!(cell < self.size[axis]);
223
224        let has_negative = self.boundary[Face::negative(axis)].has_ghost();
225        let has_positive = self.boundary[Face::positive(axis)].has_ghost();
226
227        if !has_negative && cell < border_width {
228            let right = cell;
229            Support::Negative(right)
230        } else if !has_positive && cell >= self.size[axis] - border_width {
231            let left = self.size[axis] - 1 - cell;
232            Support::Positive(left)
233        } else {
234            Support::Interior
235        }
236    }
237
238    /// Set strongly enforced boundary conditions. This enforces parity and dirichlet boundary
239    /// conditions on this particular `NodeSpace`.
240    pub fn fill_boundary(&self, extent: usize, cond: impl BoundaryConds<N>, dest: &mut [f64]) {
241        debug_assert!(is_boundary_compatible(&self.boundary, &cond));
242
243        // Loop over faces
244        for face in Face::<N>::iterate() {
245            // Enforce zeros if boundary condition is odd
246            if cond.kind(face) == BoundaryKind::AntiSymmetric {
247                // Iterate over face
248                for node in self.face_window_disjoint(face) {
249                    // For antisymmetric boundaries we set all values on axis to be 0.
250                    let index = self.index_from_node(node);
251                    dest[index] = 0.0;
252                }
253            }
254
255            if cond.kind(face) == BoundaryKind::StrongDirichlet {
256                // Iterate over face
257                for node in self.face_window_disjoint(face) {
258                    let position = self.position(node);
259                    let index = self.index_from_node(node);
260                    dest[index] = cond.dirichlet(position).target;
261                }
262            }
263        }
264
265        // Loop over regions
266        for region in regions::<N>() {
267            if region == Region::CENTRAL {
268                continue;
269            }
270
271            // Nodes to iterate over
272            let window = self.region_window(extent, region);
273
274            // Parity boundary conditions
275            let mut parity = true;
276
277            for face in region.adjacent_faces() {
278                // Flip parity if boundary is antisymmetric
279                parity ^= cond.kind(face) == BoundaryKind::AntiSymmetric;
280            }
281
282            let sign = if parity { 1.0 } else { -1.0 };
283
284            for node in window {
285                let mut source = node;
286
287                for face in region.adjacent_faces() {
288                    if !matches!(
289                        cond.kind(face),
290                        BoundaryKind::AntiSymmetric | BoundaryKind::Symmetric
291                    ) {
292                        continue;
293                    }
294
295                    if face.side {
296                        let dist = node[face.axis] - self.size[face.axis] as isize;
297                        source[face.axis] = self.size[face.axis] as isize - dist;
298                    } else {
299                        source[face.axis] = -node[face.axis];
300                    }
301                }
302
303                dest[self.index_from_node(node)] = sign * dest[self.index_from_node(source)];
304            }
305        }
306    }
307
308    /// Applies a tensor product of stencils to a given field.
309    pub fn apply(&self, corner: [isize; N], stencils: [&[f64]; N], field: &[f64]) -> f64 {
310        for axis in 0..N {
311            debug_assert!(corner[axis] >= -(self.ghost as isize));
312            debug_assert!(
313                corner[axis] <= (self.size[axis] + 1 + self.ghost - stencils[axis].len()) as isize
314            );
315        }
316
317        let ssize: [_; N] = array::from_fn(|axis| stencils[axis].len());
318
319        let mut result = 0.0;
320
321        for offset in IndexSpace::new(ssize).iter() {
322            let mut weight = 1.0;
323
324            for axis in 0..N {
325                weight *= stencils[axis][offset[axis]];
326            }
327
328            let index =
329                self.index_from_node(array::from_fn(|axis| corner[axis] + offset[axis] as isize));
330            result += field[index] * weight;
331        }
332
333        result
334    }
335
336    /// Applies a single stencil along an axis to a given field.
337    pub fn apply_axis(
338        &self,
339        corner: [isize; N],
340        stencil: &[f64],
341        field: &[f64],
342        axis: usize,
343    ) -> f64 {
344        let mut result = 0.0;
345
346        let mut node = corner;
347
348        for (i, weight) in stencil.iter().enumerate() {
349            node[axis] = corner[axis] + i as isize;
350
351            let index = self.index_from_node(node);
352            result += field[index] * weight;
353        }
354
355        result
356    }
357
358    pub fn evaluate_interior(
359        &self,
360        convolution: impl Convolution<N>,
361        node: [isize; N],
362        field: &[f64],
363    ) -> f64 {
364        let spacing = self.spacing();
365        let stencils = array::from_fn(|axis| convolution.interior(axis));
366        let corner = array::from_fn(|axis| node[axis] - convolution.border_width(axis) as isize);
367        self.apply(corner, stencils, field) * convolution.scale(spacing)
368    }
369
370    pub fn evaluate_axis_interior(
371        &self,
372        kernel: impl Kernel,
373        node: [isize; N],
374        field: &[f64],
375        axis: usize,
376    ) -> f64 {
377        let spacing = self.spacing_axis(axis);
378        let stencil = kernel.interior();
379        let mut corner = node;
380        corner[axis] -= kernel.border_width() as isize;
381        self.apply_axis(corner, stencil, field, axis) * kernel.scale(spacing)
382    }
383
384    fn stencils<'a, C: Convolution<N>>(
385        &self,
386        support: [Support; N],
387        convolution: &'a C,
388    ) -> [&'a [f64]; N] {
389        array::from_fn(move |axis| {
390            let border = match support[axis] {
391                Support::Negative(border) => Border::Negative(border),
392                Support::Positive(border) => Border::Positive(border),
393                Support::Interior => {
394                    return convolution.interior(axis);
395                }
396            };
397
398            let face = Face {
399                axis,
400                side: border.side(),
401            };
402
403            if self.boundary[face].has_ghost() {
404                convolution.interior(axis)
405            } else {
406                convolution.free(border, axis)
407            }
408        })
409    }
410
411    fn stencil_axis<'a, K: Kernel>(
412        &self,
413        support: Support,
414        kernel: &'a K,
415        axis: usize,
416    ) -> &'a [f64] {
417        let border = match support {
418            Support::Negative(border) => Border::Negative(border),
419            Support::Positive(border) => Border::Positive(border),
420            Support::Interior => {
421                return kernel.interior();
422            }
423        };
424
425        let face = Face {
426            axis,
427            side: border.side(),
428        };
429
430        if self.boundary[face].has_ghost() {
431            kernel.interior()
432        } else {
433            kernel.free(border)
434        }
435    }
436
437    /// Evaluates the operation of a convolution at a given node.
438    pub fn evaluate(
439        &self,
440        convolution: impl Convolution<N>,
441        node: [isize; N],
442        field: &[f64],
443    ) -> f64 {
444        let spacing = self.spacing();
445        let support = array::from_fn(|axis| {
446            if convolution.border_width(axis) > 0 {
447                debug_assert!(node[axis] >= 0);
448                debug_assert!(node[axis] <= self.size[axis] as isize);
449
450                self.support_axis(node[axis] as usize, convolution.border_width(axis), axis)
451            } else {
452                Support::Interior
453            }
454        });
455        let stencils = self.stencils(support, &convolution);
456        let corner = array::from_fn(|axis| match support[axis] {
457            Support::Interior => node[axis] as isize - convolution.border_width(axis) as isize,
458            Support::Negative(_) => 0,
459            Support::Positive(_) => (self.size[axis] + 1) as isize - stencils[axis].len() as isize,
460        });
461        self.apply(corner, stencils, field) * convolution.scale(spacing)
462    }
463
464    /// Evaluates the operation of a kernel along an axis at a given vertex.
465    pub fn evaluate_axis(
466        &self,
467        kernel: impl Kernel,
468        node: [isize; N],
469        field: &[f64],
470        axis: usize,
471    ) -> f64 {
472        #[cfg(debug_assertions)]
473        for axis in 0..N {
474            assert!(node[axis] <= (self.size[axis] + self.ghost) as isize);
475            assert!(node[axis] >= -(self.ghost as isize));
476        }
477
478        debug_assert!(node[axis] >= 0);
479        debug_assert!(node[axis] <= self.size[axis] as isize);
480
481        let spacing = self.spacing_axis(axis);
482        let support = self.support_axis(node[axis] as usize, kernel.border_width(), axis);
483        let stencil = self.stencil_axis(support, &kernel, axis);
484
485        let mut corner = node;
486        corner[axis] = match support {
487            Support::Interior => node[axis] - kernel.border_width() as isize,
488            Support::Negative(_) => 0,
489            Support::Positive(_) => (self.size[axis] + 1) as isize - stencil.len() as isize,
490        };
491
492        self.apply_axis(corner, stencil, field, axis) * kernel.scale(spacing)
493    }
494
495    /// Applies an interpolation kernel to the field at a given supernode.
496    pub fn prolong(&self, kernel: impl Interpolant, supernode: [isize; N], field: &[f64]) -> f64 {
497        for axis in 0..N {
498            debug_assert!(supernode[axis] <= 2 * self.size[axis] as isize);
499            debug_assert!(supernode[axis] >= 0);
500        }
501
502        let node: [_; N] = array::from_fn(|axis| supernode[axis] / 2);
503        let flags: [_; N] = array::from_fn(|axis| supernode[axis] % 2 == 1);
504
505        let mut scale = 1.0;
506
507        for _ in (0..N).filter(|&axis| flags[axis]) {
508            scale *= kernel.scale();
509        }
510
511        let support: [_; N] = array::from_fn(|axis| {
512            if flags[axis] {
513                self.support_cell_axis(node[axis] as usize, kernel.border_width(), axis)
514            } else {
515                Support::Interior
516            }
517        });
518
519        let stencils = array::from_fn(|axis| {
520            if !flags[axis] {
521                return Value.interior();
522            }
523
524            let border = match support[axis] {
525                Support::Interior => return kernel.interior(),
526                Support::Negative(i) => Border::Negative(i),
527                Support::Positive(i) => Border::Positive(i),
528            };
529
530            let face = Face {
531                axis,
532                side: border.side(),
533            };
534
535            if self.boundary[face].has_ghost() {
536                kernel.interior()
537            } else {
538                kernel.free(border)
539            }
540        });
541
542        let corner = array::from_fn(|axis| {
543            if !flags[axis] {
544                return node[axis] as isize;
545            }
546
547            match support[axis] {
548                Support::Interior => node[axis] as isize - kernel.border_width() as isize,
549                Support::Negative(_) => 0,
550                Support::Positive(_) => {
551                    self.size[axis] as isize + 1 - stencils[axis].len() as isize
552                }
553            }
554        });
555        self.apply(corner, stencils, field) * scale
556    }
557}
558
559// ****************************
560// Node Window ****************
561// ****************************
562
563/// Defines a rectagular region of a larger `NodeSpace`.
564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565pub struct NodeWindow<const N: usize> {
566    pub origin: [isize; N],
567    pub size: [usize; N],
568}
569
570impl<const N: usize> NodeWindow<N> {
571    pub fn num_nodes(&self) -> usize {
572        self.size.iter().product()
573    }
574
575    /// Iterate over all nodes in the window.
576    pub fn iter(&self) -> NodeCartesianIter<N> {
577        NodeCartesianIter::new(self.origin, self.size)
578    }
579
580    /// Iterate over nodes on a plane in the window.
581    pub fn plane(&self, axis: usize, intercept: isize) -> NodePlaneIter<N> {
582        debug_assert!(
583            intercept >= self.origin[axis]
584                && intercept < self.size[axis] as isize + self.origin[axis]
585        );
586
587        let mut size = self.size;
588        size[axis] = 1;
589
590        NodePlaneIter {
591            axis,
592            intercept,
593            inner: NodeCartesianIter::new(self.origin, size),
594        }
595    }
596}
597
598impl<const N: usize> IntoIterator for NodeWindow<N> {
599    type IntoIter = NodeCartesianIter<N>;
600    type Item = [isize; N];
601
602    fn into_iter(self) -> Self::IntoIter {
603        self.iter()
604    }
605}
606
607/// A helper for iterating over a node window.
608pub struct NodeCartesianIter<const N: usize> {
609    origin: [isize; N],
610    inner: CartesianIter<N>,
611}
612
613impl<const N: usize> NodeCartesianIter<N> {
614    pub fn new(origin: [isize; N], size: [usize; N]) -> Self {
615        Self {
616            origin,
617            inner: IndexSpace::new(size).iter(),
618        }
619    }
620}
621
622impl<const N: usize> Iterator for NodeCartesianIter<N> {
623    type Item = [isize; N];
624
625    fn next(&mut self) -> Option<Self::Item> {
626        let index = self.inner.next()?;
627
628        let mut result = self.origin;
629
630        for axis in 0..N {
631            result[axis] += index[axis] as isize;
632        }
633
634        Some(result)
635    }
636}
637
638/// A helper for iterating over a plane in a node window.
639pub struct NodePlaneIter<const N: usize> {
640    axis: usize,
641    intercept: isize,
642    inner: NodeCartesianIter<N>,
643}
644
645impl<const N: usize> Iterator for NodePlaneIter<N> {
646    type Item = [isize; N];
647
648    fn next(&mut self) -> Option<Self::Item> {
649        let mut index = self.inner.next()?;
650        index[self.axis] = self.intercept;
651        Some(index)
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use crate::{
658        geometry::HyperBox,
659        kernel::{Derivative, Interpolation},
660    };
661
662    use super::*;
663
664    fn boundary<const N: usize>() -> FaceArray<N, BoundaryClass> {
665        FaceArray::from_fn(|face| {
666            if face.side {
667                BoundaryClass::OneSided
668            } else {
669                BoundaryClass::Ghost
670            }
671        })
672    }
673
674    #[test]
675    fn support_vertex() {
676        let space = NodeSpace {
677            size: [8],
678            ghost: 3,
679            bounds: HyperBox::UNIT,
680            boundary: boundary(),
681        };
682
683        const BORDER: usize = 3;
684
685        let supports = [
686            Support::Interior,
687            Support::Interior,
688            Support::Interior,
689            Support::Interior,
690            Support::Interior,
691            Support::Interior,
692            Support::Positive(2),
693            Support::Positive(1),
694            Support::Positive(0),
695        ];
696
697        for i in 0..9 {
698            assert_eq!(space.support_axis(i, BORDER, 0), supports[i]);
699        }
700    }
701
702    #[test]
703    fn support_cell() {
704        let space = NodeSpace {
705            size: [8],
706            ghost: 3,
707            bounds: HyperBox::UNIT,
708            boundary: boundary(),
709        };
710
711        const BORDER: usize = 3;
712
713        let supports = [
714            Support::Interior,
715            Support::Interior,
716            Support::Interior,
717            Support::Interior,
718            Support::Interior,
719            Support::Positive(2),
720            Support::Positive(1),
721            Support::Positive(0),
722        ];
723
724        for i in 0..8 {
725            assert_eq!(space.support_cell_axis(i, BORDER, 0), supports[i]);
726        }
727    }
728
729    fn eval_convergence(size: [usize; 2]) -> f64 {
730        let space = NodeSpace {
731            size,
732            ghost: 2,
733            bounds: HyperBox::UNIT,
734            boundary: boundary(),
735        };
736
737        let mut field = vec![0.0; space.num_nodes()];
738
739        for node in space.full_window().iter() {
740            let index = space.index_from_node(node);
741            let [x, y] = space.position(node);
742            field[index] = x.sin() * y.sin();
743        }
744
745        let mut result: f64 = 0.0;
746
747        for node in space.inner_window().iter() {
748            let [x, y] = space.position(node);
749            let numerical = space.evaluate((Derivative::<4>, Derivative::<4>), node, &field);
750            let analytical = x.cos() * y.cos();
751            // dbg!(numerical - analytical);
752            let error: f64 = (numerical - analytical).abs();
753            result = result.max(error);
754        }
755
756        result
757    }
758
759    #[test]
760    fn evaluate() {
761        let error16 = eval_convergence([16, 16]);
762        let error32 = eval_convergence([32, 32]);
763        let error64 = eval_convergence([64, 64]);
764
765        assert!(error16 / error32 >= 16.0);
766        assert!(error32 / error64 >= 16.0);
767    }
768
769    fn prolong_convergence(size: [usize; 2]) -> f64 {
770        let cspace = NodeSpace {
771            size,
772            ghost: 2,
773            boundary: boundary(),
774            bounds: HyperBox::UNIT,
775        };
776        let rspace = NodeSpace {
777            size: size.map(|v| v * 2),
778            ghost: 2,
779            boundary: boundary(),
780            bounds: HyperBox::UNIT,
781        };
782
783        let mut field = vec![0.0; cspace.num_nodes()];
784
785        for node in cspace.full_window().iter() {
786            let index = cspace.index_from_node(node);
787            let [x, y] = cspace.position(node);
788            field[index] = x.sin() * y.sin();
789        }
790
791        let mut result: f64 = 0.0;
792
793        for node in rspace.inner_window().iter() {
794            let [x, y] = rspace.position(node);
795            let numerical = cspace.prolong(Interpolation::<4>, node, &field);
796            let analytical = x.sin() * y.sin();
797            let error: f64 = (numerical - analytical).abs();
798            result = result.max(error);
799        }
800
801        result
802    }
803
804    #[test]
805    fn prolong() {
806        let error16 = prolong_convergence([16, 16]);
807        let error32 = prolong_convergence([32, 32]);
808        let error64 = prolong_convergence([64, 64]);
809
810        assert!(error16 / error32 >= 32.0);
811        assert!(error32 / error64 >= 32.0);
812    }
813
814    #[test]
815    fn windows() {
816        let space = NodeSpace {
817            size: [10; 2],
818            ghost: 2,
819            boundary: boundary(),
820            bounds: HyperBox::UNIT,
821        };
822
823        // Regions
824        assert_eq!(
825            space.region_window(2, Region::new([Side::Left, Side::Right])),
826            NodeWindow {
827                origin: [-2, 11],
828                size: [2, 2],
829            }
830        );
831        assert_eq!(
832            space.region_window(2, Region::new([Side::Middle, Side::Right])),
833            NodeWindow {
834                origin: [0, 11],
835                size: [11, 2]
836            }
837        );
838
839        // Faces
840        assert_eq!(
841            space.face_window(Face::positive(1)),
842            NodeWindow {
843                origin: [0, 10],
844                size: [11, 1]
845            }
846        );
847
848        // Faces disjoint
849        assert_eq!(
850            space.face_window_disjoint(Face::negative(0)),
851            NodeWindow {
852                origin: [0, 0],
853                size: [1, 11],
854            }
855        );
856        assert_eq!(
857            space.face_window_disjoint(Face::negative(1)),
858            NodeWindow {
859                origin: [1, 0],
860                size: [10, 1],
861            }
862        );
863        assert_eq!(
864            space.face_window_disjoint(Face::positive(0)),
865            NodeWindow {
866                origin: [10, 1],
867                size: [1, 10],
868            }
869        );
870        assert_eq!(
871            space.face_window_disjoint(Face::positive(1)),
872            NodeWindow {
873                origin: [1, 10],
874                size: [9, 1],
875            }
876        );
877    }
878}