qtty_core/units/solid_angle/mod.rs
1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Solid-angle units.
5//!
6//! Solid angle is plane angle squared (`A²`). The canonical scaling unit for
7//! the [`Angular`](crate::dimension::Angular) dimension is the **degree**
8//! (`Degree::RATIO == 1.0`), so the canonical scaling unit for the
9//! [`SolidAngle`](crate::dimension::SolidAngle) dimension is the
10//! **square degree** (`SquareDegree::RATIO == 1.0`).
11//!
12//! All other solid-angle units are expressed as exact ratios to square
13//! degrees through the `Prod`-based composition. In particular,
14//! `Steradian::RATIO == (180/π)² ≈ 3282.806…`, since `1 sr ≈ 3282.806 deg²`.
15//!
16//! Solid-angle units arise *directly* from multiplying two angular
17//! quantities — no conversion needed:
18//!
19//! ```rust
20//! use qtty_core::angular::Degrees;
21//! use qtty_core::solid_angle::SquareDegrees;
22//!
23//! let theta = Degrees::new(2.0);
24//! let omega: SquareDegrees = theta * theta;
25//! assert!((omega.value() - 4.0).abs() < 1e-12);
26//! ```
27//!
28//! ```rust
29//! use qtty_core::angular::Radians;
30//! use qtty_core::solid_angle::Steradians;
31//!
32//! let r = Radians::new(1.0);
33//! let omega: Steradians = r * r;
34//! assert!((omega.value() - 1.0).abs() < 1e-12);
35//! ```
36
37use crate::units::angular::{Degree, Milliradian, Radian};
38use crate::{Prod, Quantity, Unit};
39
40#[cfg(feature = "astro")]
41use crate::units::angular::{Arcminute, Arcsecond};
42
43/// Re-export the solid-angle dimension from the dimension module.
44pub use crate::dimension::SolidAngle;
45
46/// Marker trait for any [`Unit`] whose dimension is [`SolidAngle`].
47pub trait SolidAngleUnit: Unit<Dim = SolidAngle> {}
48impl<T: Unit<Dim = SolidAngle>> SolidAngleUnit for T {}
49
50/// A composed solid-angle quantity from squaring an angular unit.
51///
52/// `SolidAngleOf<A>` is `Quantity<Prod<A, A>>`. Since the named solid-angle
53/// types are themselves `Prod` aliases, `SolidAngleOf<Radian>` and
54/// [`Steradians`] are the **same type**.
55///
56/// # Examples
57///
58/// ```rust
59/// use qtty_core::angular::{Degree, Degrees};
60/// use qtty_core::solid_angle::{SolidAngleOf, SquareDegrees};
61///
62/// let side = Degrees::new(5.0);
63/// let omega: SolidAngleOf<Degree> = side * side;
64/// assert!((omega.value() - 25.0).abs() < 1e-12);
65///
66/// // SolidAngleOf<Degree> IS SquareDegrees — same type:
67/// let named: SquareDegrees = omega;
68/// assert!((named.value() - 25.0).abs() < 1e-12);
69/// ```
70pub type SolidAngleOf<A> = Quantity<Prod<A, A>>;
71
72// ─────────────────────────────────────────────────────────────────────────────
73// Always-available solid-angle units (built from base angular units)
74// ─────────────────────────────────────────────────────────────────────────────
75
76/// Square degree — product of two [`Degree`] units (canonical, `RATIO == 1.0`).
77pub type SquareDegree = Prod<Degree, Degree>;
78/// A quantity measured in square degrees (= [`SolidAngleOf<Degree>`]).
79pub type SquareDegrees = Quantity<SquareDegree>;
80
81/// Steradian — product of two [`Radian`] units (`(180/π)² deg²`).
82pub type Steradian = Prod<Radian, Radian>;
83/// A quantity measured in steradians (= [`SolidAngleOf<Radian>`]).
84pub type Steradians = Quantity<Steradian>;
85
86/// Square milliradian — product of two [`Milliradian`] units.
87pub type SquareMilliradian = Prod<Milliradian, Milliradian>;
88/// A quantity measured in square milliradians.
89pub type SquareMilliradians = Quantity<SquareMilliradian>;
90
91// ─────────────────────────────────────────────────────────────────────────────
92// Astro-feature solid-angle units
93// ─────────────────────────────────────────────────────────────────────────────
94
95/// Square arcminute — product of two [`Arcminute`] units.
96#[cfg(feature = "astro")]
97pub type SquareArcminute = Prod<Arcminute, Arcminute>;
98/// A quantity measured in square arcminutes.
99#[cfg(feature = "astro")]
100pub type SquareArcminutes = Quantity<SquareArcminute>;
101
102/// Square arcsecond — product of two [`Arcsecond`] units.
103#[cfg(feature = "astro")]
104pub type SquareArcsecond = Prod<Arcsecond, Arcsecond>;
105/// A quantity measured in square arcseconds.
106#[cfg(feature = "astro")]
107pub type SquareArcseconds = Quantity<SquareArcsecond>;
108
109/// Canonical list of always-available solid-angle units.
110///
111/// Exported (`#[doc(hidden)]`) for use by the cross-dimension registry,
112/// `From` impls, and compile-time consistency checks.
113#[macro_export]
114#[doc(hidden)]
115macro_rules! solid_angle_units {
116 ($cb:path) => {
117 $cb!(SquareDegree, Steradian, SquareMilliradian);
118 };
119}
120
121// Generate bidirectional From impls between always-available solid-angle units.
122solid_angle_units!(crate::impl_unit_from_conversions);
123
124#[cfg(feature = "cross-unit-ops")]
125solid_angle_units!(crate::impl_unit_cross_unit_ops);
126
127// ── Astro-feature cross conversions ──────────────────────────────────────────
128#[cfg(feature = "astro")]
129crate::impl_unit_from_conversions_between!(
130 SquareDegree, Steradian, SquareMilliradian;
131 SquareArcminute, SquareArcsecond
132);
133
134#[cfg(all(feature = "astro", feature = "cross-unit-ops"))]
135crate::impl_unit_cross_unit_ops_between!(
136 SquareDegree, Steradian, SquareMilliradian;
137 SquareArcminute, SquareArcsecond
138);
139
140/// Canonical list of solid-angle units exposed under the `astro` feature.
141#[cfg(feature = "astro")]
142#[macro_export]
143#[doc(hidden)]
144macro_rules! solid_angle_astro_units {
145 ($cb:path) => {
146 $cb!(SquareArcminute, SquareArcsecond);
147 };
148}
149
150#[cfg(all(test, feature = "astro"))]
151solid_angle_astro_units!(crate::assert_units_are_builtin);
152
153// Compile-time check: every base solid-angle unit is registered as BuiltinUnit.
154#[cfg(test)]
155solid_angle_units!(crate::assert_units_are_builtin);
156
157#[cfg(all(test, feature = "std"))]
158mod tests {
159 use super::*;
160 use crate::angular::{Degrees, Radians};
161 use approx::assert_relative_eq;
162
163 const STERADIAN_IN_SQDEG: f64 =
164 (180.0 / core::f64::consts::PI) * (180.0 / core::f64::consts::PI);
165
166 #[test]
167 fn square_degree_is_canonical() {
168 assert_eq!(SquareDegree::RATIO, 1.0);
169 }
170
171 #[test]
172 fn steradian_to_square_degree_ratio() {
173 assert_relative_eq!(Steradian::RATIO, STERADIAN_IN_SQDEG, max_relative = 1e-12);
174 }
175
176 #[test]
177 fn radian_squared_is_steradian() {
178 let r = Radians::new(1.0);
179 let omega: Steradians = r * r;
180 assert_relative_eq!(omega.value(), 1.0, max_relative = 1e-12);
181 }
182
183 #[test]
184 fn degree_squared_is_square_degree() {
185 let d = Degrees::new(3.0);
186 let omega: SquareDegrees = d * d;
187 assert_relative_eq!(omega.value(), 9.0, max_relative = 1e-12);
188 }
189
190 #[test]
191 fn steradian_to_square_degree_conversion() {
192 let sr = Steradians::new(1.0);
193 let sqd: SquareDegrees = sr.to();
194 assert_relative_eq!(sqd.value(), STERADIAN_IN_SQDEG, max_relative = 1e-12);
195 }
196
197 #[test]
198 fn full_sphere_in_square_degrees() {
199 // Full sphere = 4π sr = 4π · (180/π)² = 41252.961… deg²
200 let full = Steradians::new(4.0 * core::f64::consts::PI);
201 let sqd: SquareDegrees = full.to();
202 assert_relative_eq!(sqd.value(), 41_252.961_249_419_3, max_relative = 1e-9);
203 }
204
205 #[test]
206 #[cfg(feature = "astro")]
207 fn square_arcsecond_to_steradian() {
208 // 1 arcsec = π/(180·3600) rad → 1 arcsec² = (π/648000)² sr
209 let one = Quantity::<SquareArcsecond>::new(1.0);
210 let sr: Steradians = one.to();
211 let expected = (core::f64::consts::PI / (180.0 * 3600.0)).powi(2);
212 assert_relative_eq!(sr.value(), expected, max_relative = 1e-12);
213 }
214
215 #[test]
216 #[cfg(feature = "astro")]
217 fn square_arcminute_in_square_degrees() {
218 let one = Quantity::<SquareArcminute>::new(3600.0);
219 let sqd: SquareDegrees = one.to();
220 // 3600 arcmin² = 1 deg² (60 arcmin = 1 deg ⇒ 3600 arcmin² = 1 deg²)
221 assert_relative_eq!(sqd.value(), 1.0, max_relative = 1e-12);
222 }
223
224 #[test]
225 fn square_milliradian_to_steradian() {
226 let one = Quantity::<SquareMilliradian>::new(1.0);
227 let sr: Steradians = one.to();
228 // 1 mrad² = 1e-6 sr
229 assert_relative_eq!(sr.value(), 1e-6, max_relative = 1e-12);
230 }
231}