star_catalog/
subcube.rs

1use geo_nd::Vector;
2
3use crate::Vec3;
4
5//tp Subcube
6/// A representation of a portion of the unit cube, to improve
7/// geometric searching of a catalog.
8///
9/// In a catalog stars can be marked as being on a part of the unit
10/// sphere. A subcube is created by dividing the cube from (-1, 1) in
11/// each dimension into ELE_PER_SIDE^3 subcubes;
12/// subcubes are easy to manipulate, and can be related to the portion
13/// of the unit sphere they encompass.
14///
15/// Not all subcubes include a portion of the unit sphere; in a
16/// catalog this would indicate there will be an empty list for that
17/// subcube. The catalog can maintain either a HashMap keyed on
18/// Subcube or a Vec indexed by subcube, of lists of stars within that
19/// subcube.
20///
21/// A subcube has up to 8 immediate neighbors (some may be out of
22/// bounds).
23///
24/// The [Subcube] provides methods for iterating over it and its
25/// neighbors, and within a region around the subcube.
26///
27/// # Rationale
28///
29/// Subdividing the unit sphere can be handled in many ways; the
30/// subcube division provides for simple iteration over space and over
31/// stars that are clustered together, which is a common search
32/// problem.
33///
34/// # Choice of ELE_PER_SIDE
35///
36/// If ELE_PER_SIDE were 2 then the maximum angle of difference
37/// between any two points in a subcube would be 90 degrees
38///
39/// If ELE_PER_SIDE were 4 the the maxim angle of difference between
40/// any two points in a subcube would be 45 degrees
41///
42/// If ELE_PER_SIDE were 32 then each subcube has side length 1/16; a
43/// maximum angle subtended of the line betwen two opposite corners
44/// and the centre of the circle as 2.asin(sqrt(3)*r/16 / r / 2) =
45/// 2.asin(sqrt(3)/32) = 6 degrees
46///
47/// Here also there is a shortest distance (minimum) of r/16 from
48/// corner of a cube, across its neighbor, to a the next neighbor (it
49/// will be more than that, but this is a mininum); this subtends an
50/// angle of 2*asin(1/16/2), or 3.58 degrees.  cos(this) is
51/// 0.99804. Hence a star within a subcube must be at least 3.58
52/// degrees from any star that is not within the subcube *or* one of
53/// its immediate neighbors - or rather, all stars within 3.58 degrees
54/// of the star MUST be within the same subcube as the star, or in one
55/// of its immediate neighbors
56///
57/// If ELE_PER_SIDE were 64 then each subcube has side length 1/32; a
58/// maximum angle subtended of the line betwen two opposite corners
59/// and the centre of the circle as 2.asin(sqrt(3)*r/32 / r / 2) =
60/// 2.asin(sqrt(3)/64) = 3 degrees
61///
62/// If ELE_PER_SIDE were 16 then theshortest distance (minimum) is
63/// r/8 from corner of a cube, across its neighbor, to a
64/// non-neighbor (it will be more than that, but this is a mininum);
65/// this subtends an angle of 2*asin(1/8/2), or 7.17 degrees
66/// cos(this) is 0.99219.
67///
68/// Emmpirically, using the Hipparcos star catalog:
69///
70/// ELE_PER_SIDE of 32 is needed for cos() > 0.9980 (3.62 degrees);
71/// 276,038 (/2) pairs of vmag<7.0
72///
73/// ELE_PER_SIDE of 16 is needed for cos() > 0.995 (5.73 degrees)
74/// There are 679082 (/2) pairs of stars of vmag < 7.0 separated by at most that
75///
76/// ELE_PER_SIDE of 8 is needed for cos() > 0.992 (7.25 degrees)
77/// There are 1080842 (/2) pairs of stars of vmag < 7.0 separated by at most that
78///
79/// ## Angles of subcubes
80///
81/// With 32 subcubes the subcube_max_angle is 6.18 degrees
82///
83/// * 2 subcubes = 12.4 degrees
84/// * 3 subcubes = 18.6 degrees
85/// * 4 subcubes = 24.8 degrees
86/// * 5 subcubes = 30.9 degrees
87/// * 6 subcubes = 37.1 degrees
88/// * 7 subcubes = 43.3 degrees
89/// * 8 subcubes = 49.5 degrees
90#[derive(Debug, Clone, Copy)]
91pub struct Subcube(u32);
92
93//ip Subcube
94impl Subcube {
95    //cp ELE_PER_SIDE
96    /// The number of subdivisions per dimension of the (-1,1) cube
97    /// that produces the subcubes
98    pub const ELE_PER_SIDE: usize = 32;
99
100    const ELE_PER_SIDE2: usize = Self::ELE_PER_SIDE * Self::ELE_PER_SIDE;
101
102    /// The number of subcubes in the (-1,1) cube
103    ///
104    /// A Catalog may have a Vec with this number of entries; each
105    /// element would be a Vec, but only those that contain stars (and
106    /// are therefore on the unit sphere) will contain elements, the
107    /// rest would be empty
108    ///
109    /// It is worth noting that as ELE_PER_SIDE increases, the number
110    /// of populated subcubes approaches 2*ELE_PER_SIDE^2
111    pub const NUM_SUBCUBES: usize = Self::ELE_PER_SIDE * Self::ELE_PER_SIDE * Self::ELE_PER_SIDE;
112
113    /// The size of each side of a Subcube
114    pub const SUBCUBE_SIZE: f64 = 2.0 / Self::ELE_PER_SIDE as f64;
115
116    /// The raduis of the circumsphere of a Subcube - i.e. all stars
117    /// within the subcube must be closer tthan this to the centre of
118    /// the Subcube (although some such stars may be in a neighboring
119    /// subcube)
120    pub const SUBCUBE_RADIUS: f64 = 1.7321 * (Self::SUBCUBE_SIZE / 2.0);
121
122    //fi index_of_coord
123    /// Get the subcube index of a coordinate
124    ///
125    /// The coordinate must be in the range -1. to 1.
126    fn index_of_coord(c: f64) -> usize {
127        let c = c.min(1.);
128        ((c + 1.0).abs() * (Self::ELE_PER_SIDE as f64) / 2.0 * 0.999_999).floor() as usize
129    }
130
131    //fi coord_of_index
132    /// Get the coordinate of the centre of an index
133    fn coord_of_index(i: usize) -> f64 {
134        (2 * i + 1) as f64 / Self::ELE_PER_SIDE as f64 - 1.0
135    }
136
137    //cp of_vector
138    /// Get the subcube of a unit vector (which is thus a point on the
139    /// unit sphere)
140    pub fn of_vector(vector: &[f64; 3]) -> Self {
141        let xe = Self::index_of_coord(vector[0]);
142        let ye = Self::index_of_coord(vector[1]);
143        let ze = Self::index_of_coord(vector[2]);
144        (xe, ye, ze).into()
145    }
146
147    //ap as_usize
148    /// Get a value for the subcube, different for each within the (-1,1) cube
149    pub fn as_usize(&self) -> usize {
150        self.0 as usize
151    }
152
153    //ap center_non_unit
154    /// Get the vector of the centre of the Subcube (this is *NOT* a unit vector)!
155    pub(crate) fn center_non_unit(&self) -> Vec3 {
156        let (x, y, z): (usize, usize, usize) = self.into();
157        [
158            Self::coord_of_index(x),
159            Self::coord_of_index(y),
160            Self::coord_of_index(z),
161        ]
162        .into()
163    }
164
165    //ap center
166    /// Get the vector of the centre of the Subcube (this is *NOT* a unit vector)!
167    pub fn center(&self) -> [f64; 3] {
168        self.center_non_unit().into()
169    }
170
171    //ap may_be_on_sphere
172    /// Return true if the subcube might contain part of the unit
173    /// sphere
174    ///
175    /// This returns false if the centre is too far from the unit
176    /// sphere for any part of the subcube to overlap the unit sphere
177    pub fn may_be_on_sphere(&self) -> bool {
178        let r = self.center_non_unit().length();
179        (1.0 - Self::SUBCUBE_RADIUS..=1.0 + Self::SUBCUBE_RADIUS).contains(&r)
180    }
181
182    //mc cos_angle_on_sphere
183    /// Return None if the subcube definitely does no intersect with the unit sphere.
184    ///
185    /// Returns Some(cos(angle)) if the subcube might intersect with
186    /// the unit sphere, where 'angle' is the angle between the
187    /// suppliid vector and the centre of the Subcube. If searching
188    /// for all Subcubes that could contain stars that are within a
189    /// certain angle of a particular unit vector then use this
190    /// function and compare the cos it returns (if any) with the cos
191    /// of (angle of search + SUBCUBE ANGLE)
192    ///
193    /// v *MUST* be a unit vector (i.e. on the unit sphere)
194    pub(crate) fn cos_angle_on_sphere(&self, v: &Vec3) -> Option<f64> {
195        let c = self.center_non_unit();
196        let r = c.length();
197        if (1.0 - Self::SUBCUBE_RADIUS..=1.0 + Self::SUBCUBE_RADIUS).contains(&r) {
198            Some(v.dot(&c) / r)
199        } else {
200            None
201        }
202    }
203
204    //mp iter_all
205    /// Get an iterator over all the Subcubes in the (-1, 1) cube
206    pub fn iter_all() -> SubcubeRangeIter {
207        SubcubeRangeIter {
208            xyz: (0, 0, 0),
209            xrange: 0..Self::ELE_PER_SIDE,
210            yrange: 0..Self::ELE_PER_SIDE,
211            zrange: 0..Self::ELE_PER_SIDE,
212        }
213    }
214
215    //mp iter_range
216    /// Get an iterator over this Subcube and all with X, Y or Z
217    /// coordinates within dxyz of it
218    ///
219    /// A value of 0 for dxyz returns an iterator over just this one
220    /// Subcube
221    ///
222    /// A value of 1 for dxyz returns an iterator over this subcube
223    /// and all its immediate neighbors
224    pub fn iter_range(&self, dxyz: usize) -> SubcubeRangeIter {
225        let xyz: (usize, usize, usize) = (*self).into();
226        let xmin = xyz.0.saturating_sub(dxyz);
227        let xmax = (xyz.0 + dxyz + 1).min(Self::ELE_PER_SIDE);
228        let ymin = xyz.1.saturating_sub(dxyz);
229        let ymax = (xyz.1 + dxyz + 1).min(Self::ELE_PER_SIDE);
230        let zmin = xyz.2.saturating_sub(dxyz);
231        let zmax = (xyz.2 + dxyz + 1).min(Self::ELE_PER_SIDE);
232        SubcubeRangeIter {
233            xyz: (xmin, ymin, zmin),
234            xrange: xmin..xmax,
235            yrange: ymin..ymax,
236            zrange: zmin..zmax,
237        }
238    }
239}
240
241//ip From<Subcube> for (usize, usize, usize)
242impl From<Subcube> for (usize, usize, usize) {
243    fn from(s: Subcube) -> (usize, usize, usize) {
244        (&s).into()
245    }
246}
247
248//ip From<&Subcube> for (usize, usize, usize)
249impl From<&Subcube> for (usize, usize, usize) {
250    fn from(s: &Subcube) -> (usize, usize, usize) {
251        let s = s.as_usize();
252        let x = s % Subcube::ELE_PER_SIDE;
253        let y = (s / Subcube::ELE_PER_SIDE) % Subcube::ELE_PER_SIDE;
254        let z = s / Subcube::ELE_PER_SIDE2;
255        (x, y, z)
256    }
257}
258
259//ip From<(usize, usize, usize)> for Subcube
260impl From<(usize, usize, usize)> for Subcube {
261    fn from((x, y, z): (usize, usize, usize)) -> Subcube {
262        let s = x + y * Self::ELE_PER_SIDE + z * Self::ELE_PER_SIDE2;
263        Subcube(s as u32)
264    }
265}
266
267//ip std::ops::Add<isize> for Subcube
268impl std::ops::Add<isize> for Subcube {
269    type Output = Subcube;
270    fn add(self, delta: isize) -> Subcube {
271        let s = self.0 as isize + delta;
272        assert!(
273            s >= 0,
274            "Delta of Subcube used to take subcube out of bounds"
275        );
276        Subcube(s as u32)
277    }
278}
279
280//tp SubcubeRangeIter
281/// Iterator over a range of Subcubes
282#[derive(Debug, Clone)]
283pub struct SubcubeRangeIter {
284    xyz: (usize, usize, usize),
285    xrange: std::ops::Range<usize>,
286    yrange: std::ops::Range<usize>,
287    zrange: std::ops::Range<usize>,
288}
289impl std::iter::Iterator for SubcubeRangeIter {
290    type Item = Subcube;
291    fn next(&mut self) -> Option<Subcube> {
292        if !self.xrange.contains(&self.xyz.0) {
293            self.xyz.0 = self.xrange.start;
294            self.xyz.1 += 1;
295        }
296        if !self.yrange.contains(&self.xyz.1) {
297            self.xyz.0 = self.xrange.start;
298            self.xyz.1 = self.yrange.start;
299            self.xyz.2 += 1;
300        }
301        if !self.zrange.contains(&self.xyz.2) {
302            return None;
303        }
304        let subcube = self.xyz.into();
305        self.xyz.0 += 1;
306        Some(subcube)
307    }
308}