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}