Skip to main content

vedaksha_math/
matrix.rs

1// Copyright © 2026 ArthIQ Labs LLC. All rights reserved.
2// Vedaksha — Vision from Vedas
3// SPDX-License-Identifier: BUSL-1.1
4// Contact: info@arthiq.net | https://vedaksha.net
5
6//! 3×3 rotation matrices for coordinate frame transformations.
7//!
8//! Provides matrix multiplication, transposition, and rotation about
9//! the X, Y, and Z axes for astronomical coordinate conversions.
10//!
11//! Source: Green, "Spherical Astronomy", Ch. 2.
12
13use libm::{cos, sin, sqrt};
14
15/// A 3×3 matrix stored in row-major order.
16#[must_use]
17#[derive(Debug, Clone, Copy)]
18pub struct Matrix3 {
19    pub data: [[f64; 3]; 3],
20}
21
22/// A 3-component vector.
23#[must_use]
24#[derive(Debug, Clone, Copy)]
25pub struct Vector3 {
26    pub x: f64,
27    pub y: f64,
28    pub z: f64,
29}
30
31impl Matrix3 {
32    /// The 3×3 identity matrix.
33    pub const IDENTITY: Self = Self {
34        data: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
35    };
36
37    /// Rotation matrix about the X axis by `theta` radians.
38    ///
39    /// ```text
40    /// Rx = | 1    0     0  |
41    ///      | 0   cos   sin |
42    ///      | 0  -sin   cos |
43    /// ```
44    pub fn rotation_x(theta: f64) -> Self {
45        let (s, c) = (sin(theta), cos(theta));
46        Self {
47            data: [[1.0, 0.0, 0.0], [0.0, c, s], [0.0, -s, c]],
48        }
49    }
50
51    /// Rotation matrix about the Y axis by `theta` radians.
52    ///
53    /// ```text
54    /// Ry = |  cos  0  -sin |
55    ///      |   0   1    0  |
56    ///      |  sin  0   cos |
57    /// ```
58    pub fn rotation_y(theta: f64) -> Self {
59        let (s, c) = (sin(theta), cos(theta));
60        Self {
61            data: [[c, 0.0, -s], [0.0, 1.0, 0.0], [s, 0.0, c]],
62        }
63    }
64
65    /// Rotation matrix about the Z axis by `theta` radians.
66    ///
67    /// ```text
68    /// Rz = |  cos  sin  0 |
69    ///      | -sin  cos  0 |
70    ///      |   0    0   1 |
71    /// ```
72    pub fn rotation_z(theta: f64) -> Self {
73        let (s, c) = (sin(theta), cos(theta));
74        Self {
75            data: [[c, s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]],
76        }
77    }
78
79    /// Returns the transpose of this matrix.
80    pub fn transpose(&self) -> Self {
81        let d = &self.data;
82        Self {
83            data: [
84                [d[0][0], d[1][0], d[2][0]],
85                [d[0][1], d[1][1], d[2][1]],
86                [d[0][2], d[1][2], d[2][2]],
87            ],
88        }
89    }
90
91    /// Returns the matrix product `self * other`.
92    #[allow(clippy::needless_range_loop)]
93    pub fn multiply(&self, other: &Self) -> Self {
94        let mut result = [[0.0_f64; 3]; 3];
95        for r in 0..3 {
96            for c in 0..3 {
97                result[r][c] = self.data[r][0] * other.data[0][c]
98                    + self.data[r][1] * other.data[1][c]
99                    + self.data[r][2] * other.data[2][c];
100            }
101        }
102        Self { data: result }
103    }
104
105    /// Applies this matrix to a vector, returning `self * v`.
106    pub fn apply(&self, v: &Vector3) -> Vector3 {
107        Vector3 {
108            x: self.data[0][0] * v.x + self.data[0][1] * v.y + self.data[0][2] * v.z,
109            y: self.data[1][0] * v.x + self.data[1][1] * v.y + self.data[1][2] * v.z,
110            z: self.data[2][0] * v.x + self.data[2][1] * v.y + self.data[2][2] * v.z,
111        }
112    }
113}
114
115impl Vector3 {
116    /// Creates a new `Vector3` with the given components.
117    pub const fn new(x: f64, y: f64, z: f64) -> Self {
118        Self { x, y, z }
119    }
120
121    /// Returns the Euclidean length (magnitude) of this vector.
122    #[must_use]
123    pub fn length(&self) -> f64 {
124        sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use core::f64::consts::{FRAC_PI_2, PI};
132
133    const EPS: f64 = 1e-12;
134
135    fn assert_vec_eq(a: &Vector3, b: &Vector3) {
136        assert!((a.x - b.x).abs() < EPS, "x: {} != {}", a.x, b.x);
137        assert!((a.y - b.y).abs() < EPS, "y: {} != {}", a.y, b.y);
138        assert!((a.z - b.z).abs() < EPS, "z: {} != {}", a.z, b.z);
139    }
140
141    fn assert_mat_eq(a: &Matrix3, b: &Matrix3) {
142        for r in 0..3 {
143            for c in 0..3 {
144                assert!(
145                    (a.data[r][c] - b.data[r][c]).abs() < EPS,
146                    "data[{}][{}]: {} != {}",
147                    r,
148                    c,
149                    a.data[r][c],
150                    b.data[r][c]
151                );
152            }
153        }
154    }
155
156    #[test]
157    fn identity_apply() {
158        let v = Vector3::new(1.0, 2.0, 3.0);
159        let result = Matrix3::IDENTITY.apply(&v);
160        assert_vec_eq(&result, &v);
161    }
162
163    #[test]
164    fn identity_multiply() {
165        let r = Matrix3::rotation_z(0.5);
166        let result = Matrix3::IDENTITY.multiply(&r);
167        assert_mat_eq(&result, &r);
168    }
169
170    #[test]
171    fn rotation_x_zero() {
172        assert_mat_eq(&Matrix3::rotation_x(0.0), &Matrix3::IDENTITY);
173    }
174
175    #[test]
176    fn rotation_x_90_rotates_y_to_neg_z() {
177        let v = Vector3::new(0.0, 1.0, 0.0);
178        let result = Matrix3::rotation_x(FRAC_PI_2).apply(&v);
179        assert_vec_eq(&result, &Vector3::new(0.0, 0.0, -1.0));
180    }
181
182    #[test]
183    fn rotation_x_preserves_length() {
184        let v = Vector3::new(1.0, 2.0, 3.0);
185        let result = Matrix3::rotation_x(1.23).apply(&v);
186        assert!((result.length() - v.length()).abs() < EPS);
187    }
188
189    #[test]
190    fn rotation_y_zero() {
191        assert_mat_eq(&Matrix3::rotation_y(0.0), &Matrix3::IDENTITY);
192    }
193
194    #[test]
195    fn rotation_y_90_rotates_z_to_neg_x() {
196        let v = Vector3::new(0.0, 0.0, 1.0);
197        let result = Matrix3::rotation_y(FRAC_PI_2).apply(&v);
198        assert_vec_eq(&result, &Vector3::new(-1.0, 0.0, 0.0));
199    }
200
201    #[test]
202    fn rotation_z_zero() {
203        assert_mat_eq(&Matrix3::rotation_z(0.0), &Matrix3::IDENTITY);
204    }
205
206    #[test]
207    fn rotation_z_90_rotates_x_to_neg_y() {
208        let v = Vector3::new(1.0, 0.0, 0.0);
209        let result = Matrix3::rotation_z(FRAC_PI_2).apply(&v);
210        assert_vec_eq(&result, &Vector3::new(0.0, -1.0, 0.0));
211    }
212
213    #[test]
214    fn transpose_identity() {
215        assert_mat_eq(&Matrix3::IDENTITY.transpose(), &Matrix3::IDENTITY);
216    }
217
218    #[test]
219    fn rotation_transpose_is_inverse() {
220        let r = Matrix3::rotation_x(0.7);
221        let result = r.multiply(&r.transpose());
222        assert_mat_eq(&result, &Matrix3::IDENTITY);
223    }
224
225    #[test]
226    fn double_transpose_is_original() {
227        let r = Matrix3::rotation_y(1.1);
228        assert_mat_eq(&r.transpose().transpose(), &r);
229    }
230
231    #[test]
232    fn multiply_associative() {
233        let a = Matrix3::rotation_x(0.3);
234        let b = Matrix3::rotation_y(0.5);
235        let c = Matrix3::rotation_z(0.7);
236        let ab_c = a.multiply(&b).multiply(&c);
237        let a_bc = a.multiply(&b.multiply(&c));
238        assert_mat_eq(&ab_c, &a_bc);
239    }
240
241    #[test]
242    fn full_rotation_is_identity() {
243        assert_mat_eq(&Matrix3::rotation_z(2.0 * PI), &Matrix3::IDENTITY);
244    }
245
246    #[test]
247    fn composition_equals_sum() {
248        let a = 0.4_f64;
249        let b = 0.6_f64;
250        let composed = Matrix3::rotation_x(a).multiply(&Matrix3::rotation_x(b));
251        let direct = Matrix3::rotation_x(a + b);
252        assert_mat_eq(&composed, &direct);
253    }
254
255    #[test]
256    fn vector_length() {
257        let v = Vector3::new(3.0, 4.0, 0.0);
258        assert!((v.length() - 5.0).abs() < EPS);
259    }
260}