rao/
lib.rs

1//! # rao
2//! 
3//! `rao` - Adaptive Optics tools in Rust - is a set of fast and robust adaptive
4//! optics utilities. The current scope of `rao` is for the calculation of
5//! large matrices in AO, used in the configuration of real-time adaptive optics,
6//! control. Specifically, we aim to provide fast, scalable, and reliable APIs for
7//! generating:
8//!  - `rao::IMat` - the interaction matrix between measurements and actuators,
9//!  - `rao::CovMat` - the covariance matrix between measurements.
10//!
11//! These two matrices are typically the largest computational burden in the
12//! configuration of real-time control (RTC) for AO, and also the most 
13//! performance-sensitive parts of the RTC.
14//!
15//! # Examples
16//! Building an interaction matrix for a square-grid DM and a square-grid SH-WFS:
17//! ```
18//! use crate::rao::Matrix;
19//! const N_SUBX: i32 = 8;  // 8 x 8 subapertures
20//! const PITCH: f64 = 0.2;  // 0.2 metres gap between actuators
21//! const COUPLING: f64 = 0.5;  // actuator cross-coupling
22//! 
23//! // build list of measurements
24//! let mut measurements = vec![];
25//! for i in 0..N_SUBX {
26//!     for j in 0..N_SUBX {
27//!         let x0 = ((j-N_SUBX/2) as f64 + 0.5)*PITCH;
28//!         let y0 = ((i-N_SUBX/2) as f64 + 0.5)*PITCH;
29//!         let xz = 0.0;  // angular x-component (radians)
30//!         let yz = 0.0;  // angular y-compenent (radians)
31//!         // define the optical axis of subaperture
32//!         let line = rao::Line::new(x0,xz,y0,yz);
33//!         // slope-style measurement
34//!         // x-slope
35//!         measurements.push(rao::Measurement::SlopeTwoEdge{
36//!             central_line: line.clone(),
37//!             edge_separation: PITCH,
38//!             edge_length: PITCH,
39//!             npoints: 5,
40//!             gradient_axis: rao::Vec2D::x_unit(),
41//!             altitude: f64::INFINITY,
42//!         });
43//!         // y-slope
44//!         measurements.push(rao::Measurement::SlopeTwoEdge{
45//!             central_line: line.clone(),
46//!             edge_separation: PITCH,
47//!             edge_length: PITCH,
48//!             npoints: 5,
49//!             gradient_axis: rao::Vec2D::y_unit(),
50//!             altitude: f64::INFINITY,
51//!         });
52//!     }
53//! }
54//! 
55//! // build list of actuators
56//! let mut actuators = vec![];
57//! for i in 0..(N_SUBX+1) {
58//!     for j in 0..(N_SUBX+1) {
59//!         let x = ((j-N_SUBX/2) as f64)*PITCH;
60//!         let y = ((i-N_SUBX/2) as f64)*PITCH;
61//!         actuators.push(
62//!             // Gaussian influence functions
63//!             rao::Actuator::Gaussian{
64//!                 // std defined by coupling and pitch
65//!                 sigma: rao::coupling_to_sigma(COUPLING, PITCH),
66//!                 // position of actuator in 3D (z=altitude)
67//!                 position: rao::Vec3D::new(x, y, 0.0),
68//!             }
69//!         );
70//!     }
71//! }
72//! 
73//! // instanciate imat from (actu,meas)
74//! let imat = rao::IMat::new(&measurements, &actuators);
75//! // serialise imat for saving
76//! let data: Vec<f64> = imat.flattened_array();
77//! ```
78
79#[macro_use] extern crate impl_ops;
80
81pub mod utils;
82pub use utils::coupling_to_sigma;
83mod geometry;
84pub use crate::geometry::{
85    Vec2D,
86    Vec3D,
87    Line,
88};
89mod linalg;
90pub use crate::linalg::Matrix;
91mod core;
92pub use crate::core::{
93    Sampler,
94    Sampleable,
95    CoSampleable,
96    IMat,
97    CovMat,
98};
99
100
101/// Common [Sampler]s in Adaptive Optics
102/// 
103/// A [Measurement] provides a scalar-valued sample of an AO system. A single
104/// measurement device (e.g., a Shack Hartmann WFS) is typically comprised of 
105/// many [Measurement]s, e.g., `&[Measurement; N]`.
106#[derive(Debug,Clone)]
107pub enum Measurement{
108    /// The null measurement, always returning 0.0 regardless of the measured object.
109    Zero,
110    /// Phase measurement along a given [Line].
111    Phase {
112        /// [Line] in 3D space to trace through [Sampleable] objects.
113        line: Line,
114    },
115    /// Slope measurement, defined by two [Line]s in 3D space.
116    ///
117    /// The slope measured is equal to the *sampled function at the point where
118    /// `line_pos` intersects it* minus the *sampled function at the point
119    /// where `line_neg` intersects it*, divided by the *distance between the
120    /// two lines at zero-altitude*.
121    SlopeTwoLine {
122        /// positive end of slope (by convention)
123        line_pos: Line,
124        /// negative end of slope (by convention)
125        line_neg: Line,
126    },
127    /// Slope measurement, defined by a [Line] in 3D space and  the sampled edges
128    /// a rectangle in 2D space.
129    ///
130    /// Approximate the average slope over a region defined by two edges of a rectangle.
131    /// The edges are defined by their length, their separation, and the axis along which
132    /// they are separated (the *gradient axis*). The edges of the rectangle are sampled
133    /// so that the spacing between points is uniform and the furthest distance to an
134    /// unsampled point of the edge is minimal, i.e.:
135    ///  - `npoints=1` => `[----x----]`,
136    ///  - `npoints=2` => `[--x---x--]`,
137    ///  - `npoints=3` => `[-x--x--x-]`,
138    ///  - *etc*
139    ///
140    /// This is not a typical "linspace", which is ill-defined for 1 point, though one
141    /// could implement that as a [Sampler] wrapper around [`Measurement::SlopeTwoLine`].
142    SlopeTwoEdge {
143        /// Principle axis of the WFS
144        central_line: Line,
145        /// Length of edges (e.g., subaperture height for x-slope measurement)
146        edge_length: f64,
147        /// Separation of edges (e.g., subaperture width for x-slope measurement)
148        edge_separation: f64,
149        /// Axis of gradient vector to sample, e.g., x-slope -> `Vec2D::new(1.0, 0.0)`
150        gradient_axis: Vec2D,
151        /// Number of points to sample along each edge (more points can be more accurate).
152        npoints: u32,
153        /// Altitude of target to raytrace through to.
154        altitude: f64,
155    },
156}
157
158/// [Measurement] is the prototypical [Sampler] type.
159impl Sampler for Measurement {
160    /// The [`Sampler::get_bundle`] method implementation for the [Measurement] variants
161    /// should serve as an example for implementing other [Sampler] types. Inspect the
162    /// source code for the reference implementation. In short:
163    ///  - a [`Measurement::Zero`] variant returns an empty vector,
164    ///  - a [`Measurement::Phase`] variant returns a single line with a coefficient of `1.0`,
165    ///  - a [`Measurement::SlopeTwoLine`] variant returns two lines, with a positive
166    ///    and negative coefficient (for the *start* and *end* of the slope), scaled by
167    ///    the inverse of the ground separation of the lines, so that the resulting units
168    ///    are in *sampled function units per distance unit*.
169    ///  - a [`Measurement::SlopeTwoEdge`] variant returns `2 * npoints` lines, consisting of
170    ///    `npoints` positive coefficients, and `npoints` negative coefficients, each scaled
171    ///    by the inverse of the ground-layer separation and a factor of `1.0 / npoints as f64`,
172    ///    in order to get a result which is in units of *sampled function units per distance unit*. 
173    fn get_bundle(&self) -> Vec<(Line,f64)> {
174        match self {
175            Measurement::Zero => vec![],
176            Measurement::Phase{line} => vec![(line.clone(),1.0)],
177            Measurement::SlopeTwoLine{line_pos, line_neg} => {
178                let coeff = 1.0/line_pos.distance_at_ground(line_neg);
179                vec![
180                    (line_pos.clone(),  coeff),
181                    (line_neg.clone(), -coeff),
182                ]
183            },
184            Measurement::SlopeTwoEdge{central_line, edge_length, edge_separation, gradient_axis, npoints, altitude} => {
185                let coeff = (1.0 / f64::from(*npoints)) / edge_separation;
186                let offset_vec = gradient_axis * edge_separation * 0.5;
187                let point_a =  edge_length * 0.5 * gradient_axis.ortho();
188                let point_b = -point_a.clone();
189                match *altitude {
190                    f64::INFINITY => {
191                        Vec2D::linspread(&point_a, &point_b, *npoints)
192                        .iter()
193                        .flat_map(|p|
194                            vec![
195                                (
196                                    central_line + (p + &offset_vec),
197                                    coeff
198                                ),
199                                (
200                                    central_line + (p - &offset_vec),
201                                    -coeff
202                                ),
203                            ])
204                        .collect()
205                    },
206                    altitude => {
207                        Vec2D::linspread(&point_a, &point_b, *npoints)
208                        .iter()
209                        .flat_map(|p| {
210                            let p_alt = Vec3D::new(central_line.xz*altitude, central_line.yz*altitude, altitude);
211                            vec![
212                                (
213                                    Line::new_from_two_points(
214                                        &((central_line + (p + &offset_vec)).position_at_altitude(0.0) + Vec3D::origin()),
215                                        &p_alt,
216                                    ),
217                                    coeff
218                                ),
219                                (
220                                    Line::new_from_two_points(
221                                        &((central_line + (p - &offset_vec)).position_at_altitude(0.0) + Vec3D::origin()),
222                                        &p_alt,
223                                    ),
224                                    -coeff
225                                ),
226                            ]})
227                        .collect()
228                    }
229                }
230            },
231        }
232    }
233}
234
235
236
237/// Common [Sampleable]s in Adaptive Optics.
238/// 
239/// An [Actuator]'s state is defined by a scalar value, so a device with `N`
240/// actuatable degrees of freedom is considered as `N` different [Actuator]s,
241/// e.g., `&[Actuator; N]`.
242#[derive(Debug,Clone)]
243pub enum Actuator{
244    /// A null actuator, making zero impact on any `Measurement`
245    Zero,
246    /// A circularly symmetric Gaussian actuator, centred at `position` with
247    /// a specified scalar `sigma`. See [`utils::gaussian`] for more info.
248    Gaussian {
249        /// sigma of gaussian function in metres. 
250        sigma: f64,
251        /// position of actuator in 3d space, z=altitude.
252        position: Vec3D,
253    },
254    TipTilt {
255        /// position along slope of TT actuator surface where the amplitude
256        /// of the phase is equal to +1.0 units. This vector is colinear with
257        /// the acuation axis. E.g., if this vector is `(1.0,0.0)`, then the 
258        /// response of the actuator will be a 
259        ///Deliberately not in arcseconds
260        /// so that you have to be deliberate and careful about your units.
261        unit_response: Vec2D,
262    },
263}
264
265impl Sampleable for Actuator {
266    fn sample(&self, line: &Line) -> f64 {
267        match self {
268            Self::Zero => 0.0,
269            Self::Gaussian{sigma, position} => {
270                let distance = position.distance_at_altitude(line);
271                utils::gaussian(distance / sigma)
272            },
273            Self::TipTilt{unit_response} => {
274                line.position_at_altitude(0.0).dot(unit_response)
275            }
276        }
277    }
278}
279
280/// Simple covariance model, this might be refactored into an enum of models.
281#[derive(Debug,Clone)]
282pub struct VonKarmanLayer {
283    pub r0: f64,
284    pub l0: f64,
285    pub alt: f64,
286    pub v: Vec2D,
287}
288
289impl VonKarmanLayer {
290    /// Construct a new von Karman turbulence layer from its parameters
291    #[must_use]
292    pub fn new(r0: f64, l0: f64, alt: f64, v: Vec2D) -> VonKarmanLayer {
293        VonKarmanLayer {
294            r0, l0, alt, v,
295        }
296    }
297}
298
299/// [`VonKarmanLayer`] is (for now) the prototypical [`CoSampleable`] object.
300///
301/// Perhaps confusingly, this implementation allows the cosampling of the
302/// von Karman turbulence statistical model, returning the covariance between
303/// two [`Line`]s intercepting that layer.
304impl CoSampleable for VonKarmanLayer {
305    fn cosample(&self, line_a: &Line, line_b:&Line, dt: f64) -> f64 {
306        let p1 = line_a.position_at_altitude(self.alt);
307        let p2 = line_b.position_at_altitude(self.alt) - dt * self.v.clone();
308        utils::vk_cov((p1-p2).norm(), self.r0, self.l0)
309    }
310}
311
312
313#[derive(Debug,Clone)]
314pub struct Pupil {
315    pub rad_outer: f64,
316    pub rad_inner: f64,
317    pub spider_thickness: f64,
318    pub spiders: Vec<(Vec2D,Vec2D)>
319}
320
321impl Sampleable for Pupil {
322    fn sample(&self, ell: &Line) -> f64 {
323        let p = ell.position_at_altitude(0.0);
324        let mut out: f64 = 1.0;
325        let r = p.norm();
326        if r > self.rad_outer {
327            out *= 0.0;
328        }
329        if r < self.rad_inner {
330            out *= 0.0;
331        }
332        for spider in &self.spiders {
333            if signed_distance_to_capsule(
334                &p, &spider.0, &spider.1, self.spider_thickness/2.0
335            ) < 0.0 {
336                out *= 0.0;
337            }
338        }
339        out
340    }
341}
342
343fn signed_distance_to_capsule(
344    position: &Vec2D,
345    capsule_start: &Vec2D,
346    capsule_end: &Vec2D,
347    radius: f64
348) -> f64 {
349    let pa: Vec2D = position - capsule_start;
350    let ba: Vec2D = capsule_end - capsule_start;
351    let mut h: f64 = pa.dot(&ba)/ba.dot(&ba);
352    h = h.clamp(0.0, 1.0);
353    (pa - ba*h).norm() - radius
354}
355
356
357
358#[cfg(test)]
359mod tests {
360    use std::f64;
361
362    use super::*;
363    use approx::{assert_abs_diff_eq, assert_abs_diff_ne};
364    
365    #[test]
366    fn gaussian_on_axis_phase() {
367        let actuators = [
368            Actuator::Gaussian{
369                sigma: coupling_to_sigma(0.5,1.0),
370                position: Vec3D::new(0.0, 0.0, 0.0),
371            }
372        ];
373        let measurements = [
374            Measurement::Phase{
375                line: Line::new_on_axis(0.0, 0.0)
376            }
377        ];
378        let imat = IMat::new(&measurements, &actuators);
379        assert_abs_diff_eq!(imat.eval(0,0), 1.0, epsilon = f64::EPSILON);
380    }
381
382    #[test]
383    fn gaussian_off_axis_phase() {
384        let actuators = [
385            Actuator::Gaussian{
386                sigma: coupling_to_sigma(0.5,1.0),
387                position: Vec3D::new(0.0, 0.0, 1000.0),
388            }
389        ];
390        let measurements = [
391            Measurement::Phase{
392                line: Line::new(0.0, 1.0/1000.0, 0.0, 0.0)
393            }
394        ];
395        let imat = IMat::new(&measurements, &actuators);
396        assert_abs_diff_eq!(imat.eval(0,0), 0.5, epsilon = f64::EPSILON);
397    }
398
399    #[test]
400    fn gaussian_off_axis_phase_twopoint() {
401        let actuators = [
402            Actuator::Gaussian{
403                sigma: coupling_to_sigma(0.5,1.0),
404                position: Vec3D::new(0.0, 0.0, 1000.0),
405            }
406        ];
407        let measurements = [
408            Measurement::Phase{
409                line: Line::new_from_two_points(
410                    &Vec3D::new(1.0,1.0,0.0),
411                    &Vec3D::new(1.0,-1.0,2000.0),
412                )
413            }
414        ];
415        let imat = IMat::new(&measurements, &actuators);
416        assert_abs_diff_eq!(imat.eval(0,0), 0.5);
417    }
418
419    #[test]
420    fn simple_symmetric() {
421        let actuators = [
422            Actuator::Gaussian{
423                sigma: coupling_to_sigma(0.5,1.0),
424                position: Vec3D::new(0.0, 0.0, 1000.0),
425            },
426            Actuator::Gaussian{
427                sigma: coupling_to_sigma(0.5,1.0),
428                position: Vec3D::new(1.0, 0.0, 1000.0),
429            },
430        ];
431        let measurements = [
432            Measurement::Phase{
433                line: Line::new_on_axis(0.0, 0.0),
434            },
435            Measurement::Phase{
436                line: Line::new(1.0, 0.0, 0.0, 0.0),
437            }
438        ];
439        let imat = IMat::new(&measurements, &actuators);
440        assert!(imat.eval(0,0)>0.0);
441        assert_abs_diff_eq!(imat.eval(0,0),imat.eval(1,1));
442        assert_abs_diff_eq!(imat.eval(1,0),imat.eval(0,1));
443    }
444
445    #[test]
446    fn slope_twopoint() {
447        let actuators = [
448            Actuator::Gaussian{
449                sigma: coupling_to_sigma(0.5,1.0),
450                position: Vec3D::origin(),
451            }
452        ];
453        let line = Line::new(1.0, 0.0, 0.0, 0.0);
454        let measurements = [
455            Measurement::SlopeTwoLine{
456                line_neg: &line+Vec2D::new(-1e-5, 0.0),
457                line_pos: &line+Vec2D::new(1e-5, 0.0)
458            },
459            Measurement::SlopeTwoLine{
460                line_neg: &line+Vec2D::new(-1e-6, 0.0),
461                line_pos: &line+Vec2D::new(1e-6, 0.0)
462            },
463            Measurement::Phase{
464                line: Line::new(1.0+1e-7, 0.0, 0.0, 0.0),
465            },
466            Measurement::Phase{
467                line: Line::new(1.0-1e-7, 0.0, 0.0, 0.0),
468            },
469        ];
470        let imat = IMat::new(&measurements, &actuators);
471        assert_abs_diff_eq!(imat.eval(0,0),imat.eval(1,0),epsilon=1e-8);
472        assert_abs_diff_eq!(
473            (imat.eval(2,0)-imat.eval(3,0))/2e-7,
474            imat.eval(1,0),
475            epsilon=1e-8
476        );
477    }
478
479
480    #[test]
481    fn slope_twoedge() {
482        let actuators = [
483            Actuator::Gaussian{
484                sigma: coupling_to_sigma(0.5,1.0),
485                position: Vec3D::origin(),
486            }
487        ];
488
489        let line = Line::new(1.0, 0.0, 0.0, 0.0);
490        let measurements = [
491            Measurement::SlopeTwoLine{
492                line_neg: &line+Vec2D::new(-1e-2, 0.0),
493                line_pos: &line+Vec2D::new(1e-2, 0.0),
494            },
495            Measurement::SlopeTwoEdge{
496                central_line: line,
497                edge_length: 0.0,
498                edge_separation: 2e-2,
499                gradient_axis: Vec2D::x_unit(),
500                npoints: 100,
501                altitude: f64::INFINITY,
502            }
503        ];
504        let imat = IMat::new(&measurements, &actuators);
505        assert_abs_diff_eq!(imat.eval(0,0),imat.eval(1,0),epsilon=1e-10);
506    }
507
508    #[test]
509    fn slope_tt_twopoint() {
510        let actuators = [
511            Actuator::TipTilt{
512                unit_response: Vec2D::x_unit()
513            }
514        ];
515        let line = Line::new(1.0, 0.0, 0.0, 0.0);
516        let measurements = [
517            Measurement::SlopeTwoLine{
518                line_neg: &line+Vec2D::new(-1e-6, 0.0),
519                line_pos: &line+Vec2D::new(1e-6, 0.0)
520            },
521            Measurement::SlopeTwoLine{
522                line_neg: &line+Vec2D::new(-1e-7, 0.0),
523                line_pos: &line+Vec2D::new(1e-7, 0.0)
524            },
525        ];
526        let imat = IMat::new(&measurements, &actuators);
527        assert_abs_diff_eq!(imat.eval(0,0),imat.eval(1,0),epsilon=1e-8);
528    }
529
530    #[test]
531    fn tt_on_axis_phase() {
532        let actuators = [
533            Actuator::TipTilt{
534                unit_response: Vec2D::x_unit()
535            }
536        ];
537        let measurements = [
538            Measurement::Phase {
539                line: Line::new_on_axis(0.0, 0.0)
540            }
541        ];
542        let imat = IMat::new(&measurements, &actuators);
543        assert_abs_diff_eq!(imat.eval(0,0), 0.0, epsilon = f64::EPSILON);
544    }
545
546    #[test]
547    fn tt_off_axis_phase() {
548        let actuators = [
549            Actuator::TipTilt{
550                unit_response: Vec2D::x_unit()
551            }
552        ];
553        let measurements = [
554            Measurement::Phase{
555                line: Line::new(2.0, 0.0, 0.0, 0.0)
556            }
557        ];
558        let imat = IMat::new(&measurements, &actuators);
559        assert_abs_diff_eq!(imat.eval(0,0), 2.0, epsilon = f64::EPSILON);
560    }
561
562    #[test]
563    fn test_vkcov() {
564        let vk = VonKarmanLayer {
565            r0: 0.1,
566            l0: 25.0,
567            alt: 1000.0,
568            v: Vec2D { x: 10.0, y: 0.0 }
569        };
570        let line = Line::new_on_axis(0.0,0.0);
571        let a = vk.cosample(&line, &line, 0.0);
572        assert_abs_diff_eq!(a,utils::vk_cov(0.0, vk.r0, vk.l0));
573        assert_abs_diff_eq!(a,856.346_613_137_351_7,epsilon=1e-3);
574        let a = vk.cosample(&line, &line, 1.0);
575        assert_abs_diff_ne!(a,856.346_613_137_351_7,epsilon=1e-3);
576    }
577
578    #[test]
579    fn test_vkcovmat() {
580        let vk = VonKarmanLayer {
581            r0: 0.1,
582            l0: 25.0,
583            alt: 1000.0,
584            v: Vec2D { x: 10.0, y: 0.0 }
585        };
586        let measurements: Vec<Measurement> = (0..10)
587        .map(|i| f64::from(i) * 0.8)
588        .map(|x|
589            Measurement::Phase{
590                line: Line::new_on_axis(x, 0.0)
591            }
592        ).collect();
593        let covmat = CovMat::new(&measurements, &measurements, &vk, 1e-3);
594        println!("{covmat}");
595    }
596    
597    #[test]
598    fn test_mixedvkcovmat() {
599        let vk = VonKarmanLayer {
600            r0: 0.1,
601            l0: 25.0,
602            alt: 1000.0,
603            v: Vec2D{ x: 10.0, y: 0.0 }
604        };
605        let line = Line::new_on_axis(0.0, 0.0);
606        let measurements = [
607            Measurement::Phase {
608                line: line.clone(),
609            },
610            Measurement::SlopeTwoLine {
611                line_pos: &line+Vec2D::new(0.1,0.0),
612                line_neg: &line+Vec2D::new(-0.1,0.0),
613            },
614            Measurement::SlopeTwoEdge {
615                central_line: line.clone(),
616                edge_separation: 0.2,
617                edge_length: 0.2,
618                gradient_axis: Vec2D::x_unit(),
619                npoints: 10,
620                altitude: f64::INFINITY,
621            },
622        ];
623        let covmat = CovMat::new(&measurements, &measurements, &vk, 1e-3);
624        println!("{covmat}");
625    }
626
627    #[test]
628    fn make_pupil() {
629        let pup = [Pupil{
630            rad_outer: 4.0,
631            rad_inner: 0.5,
632            spider_thickness: 0.1,
633            spiders: vec![
634                (Vec2D::new(-4.0,-4.0),Vec2D::new(3.0,0.0)),
635                (Vec2D::new(-4.0,-4.0),Vec2D::new(3.0,0.0)),
636                (Vec2D::new(-4.0,-4.0),Vec2D::new(3.0,0.0)),
637                (Vec2D::new(-4.0,-4.0),Vec2D::new(3.0,0.0)),
638                ],
639        }];
640        const NPOINTS: u32 = 1000;
641        let x = Vec2D::linspread(
642            &Vec2D::new(-4.0, 0.0),
643            &Vec2D::new(4.0, 0.0),
644            NPOINTS,
645        );
646        let y = Vec2D::linspread(
647            &Vec2D::new(0.0, -4.0),
648            &Vec2D::new(0.0, 4.0),
649            NPOINTS,
650        );
651        println!("{y:?}");
652        let p: Vec<Measurement> = y.iter().flat_map(|y|
653            x.iter().map(|x| Measurement::Phase{
654                line:Line::new(x.x, 0.0, y.y, 0.0)
655            })).collect();
656        let pup_vec = IMat::new(&p, &pup);
657        let shape = [NPOINTS as usize, NPOINTS as usize];
658        let data: Vec<f64> = pup_vec.flattened_array();
659        let primary_hdu = fitrs::Hdu::new(&shape, data);
660        fitrs::Fits::create("/tmp/pup.fits", primary_hdu)
661        .expect("Failed to create");
662    }
663}