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}