nalgebra_spacetime/
lib.rs

1//! Spacetime Extension for [nalgebra]
2//!
3//! [nalgebra]: https://nalgebra.org
4//!
5//! # Present Features
6//!
7//!   * Minkowski space as special case of $n$-dimensional [`Lorentzian`] space.
8//!   * Raising/Lowering tensor indices:
9//!     [`Lorentzian::dual`]/[`Lorentzian::r_dual`]/[`Lorentzian::c_dual`].
10//!   * Metric contraction of degree-1/degree-2 tensors:
11//!     [`Lorentzian::contr`]/[`Lorentzian::scalar`].
12//!   * Spacetime [`Lorentzian::interval`] with [`LightCone`] depiction.
13//!   * Inertial [`OFrame`] of reference holding boost parameters.
14//!   * Lorentz boost as [`Lorentzian::new_boost`] matrix.
15//!   * Direct Lorentz [`Lorentzian::boost`] to [`OFrame::compose`] velocities.
16//!   * Wigner [`OFrame::rotation`] and [`OFrame::axis_angle`] between to-be-composed boosts.
17//!
18//! # Future Features
19//!
20//!   * `Event4`/`Velocity4`/`Momentum4`/`...` equivalents of `Point4`/`...`.
21//!   * Categorize `Rotation4`/`PureBoost4`/`...` as `Boost4`/`...`.
22//!   * Wigner [`OFrame::rotation`] and [`OFrame::axis_angle`] of an already-composed `Boost4`.
23//!   * Distinguish pre/post-rotation and active/passive `Boost4` compositions.
24
25#![forbid(unsafe_code)]
26#![forbid(missing_docs)]
27#![allow(clippy::doc_markdown)]
28#![allow(clippy::type_complexity)]
29
30pub use approx;
31pub use nalgebra;
32
33use approx::{abs_diff_eq, AbsDiffEq};
34use nalgebra::{
35	base::allocator::Allocator,
36	base::dimension::{U1, U2, U3, U4, U5, U6},
37	constraint::{
38		AreMultipliable, DimEq, SameDimension, SameNumberOfColumns, SameNumberOfRows,
39		ShapeConstraint,
40	},
41	storage::{Owned, RawStorage, RawStorageMut, Storage, StorageMut},
42	DefaultAllocator, Dim, DimName, DimNameDiff, DimNameSub, Matrix, MatrixView, MatrixViewMut,
43	OMatrix, OVector, RealField, Scalar, Unit, VectorView,
44};
45use std::ops::{Add, Neg, Sub};
46
47#[inline]
48fn neg<T: RealField>(n: &mut T) {
49	*n = -n.clone();
50}
51
52/// Extension for $n$-dimensional Lorentzian space $\R^{-,+} = \R^{1,n-1}$ with
53/// metric signature in spacelike sign convention.
54///
55/// In four dimensions also known as Minkowski space $\R^{-,+} = \R^{1,3}$.
56///
57/// A statically sized column-major matrix whose `R` rows and `C` columns
58/// coincide with degree-1/degree-2 tensor indices.
59pub trait Lorentzian<T, R, C>
60where
61	T: Scalar,
62	R: DimName,
63	C: DimName,
64{
65	/// Lorentzian metric tensor $\eta_{\mu \nu}$:
66	///
67	/// $$
68	/// \eta_{\mu \nu} = \begin{pmatrix}
69	///   -1   &    0   &  \dots &    0   \\\\
70	///    0   &    1   & \ddots & \vdots \\\\
71	/// \vdots & \ddots & \ddots &    0   \\\\
72	///    0   &  \dots &    0   &    1   \end{pmatrix}
73	/// $$
74	///
75	/// Avoid matrix multiplication by preferring:
76	///
77	///   * [`Self::dual`], [`Self::r_dual`] or [`Self::c_dual`] and their in-place counterparts
78	///   * [`Self::dual_mut`], [`Self::r_dual_mut`] or [`Self::c_dual_mut`].
79	///
80	/// The spacelike sign convention $\R^{-,+} = \R^{1,n-1}$ requires less negations than its
81	/// timelike alternative $\R^{+,-} = \R^{1,n-1}$. In four dimensions or Minkowski space
82	/// $\R^{-,+} = \R^{1,3}$ it requires:
83	///
84	///   * $n - 2 = 2$ less for degree-1 tensors, and
85	///   * $n (n - 2) = 8$ less for one index of degree-2 tensors, but
86	///   * $0$ less for two indices of degree-2 tensors.
87	///
88	/// Choosing the component order of $\R^{-,+} = \R^{1,n-1}$ over $\R^{+,-} = \R^{n-1,1}$
89	/// identifies the time component of $x^\mu$ as $x^0$ in compliance with the convention of
90	/// identifying spatial components $x^i$ with Latin alphabet indices starting from $i=1$.
91	/// ```
92	/// use approx::assert_ulps_eq;
93	/// use nalgebra::{Matrix4, Vector4};
94	/// use nalgebra_spacetime::Lorentzian;
95	///
96	/// let eta = Matrix4::<f64>::metric();
97	/// let sc = Vector4::new(-1.0, 1.0, 1.0, 1.0);
98	/// assert_ulps_eq!(eta, Matrix4::from_diagonal(&sc));
99	///
100	/// let x = Vector4::<f64>::new_random();
101	/// assert_ulps_eq!(x.dual(), eta * x);
102	///
103	/// let f = Matrix4::<f64>::new_random();
104	/// assert_ulps_eq!(f.dual(), eta * f * eta);
105	/// assert_ulps_eq!(f.dual(), f.r_dual().c_dual());
106	/// assert_ulps_eq!(f.dual(), f.c_dual().r_dual());
107	/// assert_ulps_eq!(f.r_dual(), eta * f);
108	/// assert_ulps_eq!(f.c_dual(), f * eta);
109	/// ```
110	#[must_use]
111	fn metric() -> Self
112	where
113		ShapeConstraint: SameDimension<R, C>;
114
115	/// Raises/Lowers *all* of its degree-1/degree-2 tensor indices.
116	///
117	/// Negates the appropriate components avoiding matrix multiplication.
118	#[must_use]
119	fn dual(&self) -> Self;
120
121	/// Raises/Lowers its degree-1/degree-2 *row* tensor index.
122	///
123	/// Prefer [`Self::dual`] over `self.r_dual().c_dual()` to half negations.
124	#[must_use]
125	fn r_dual(&self) -> Self;
126
127	/// Raises/Lowers its degree-1/degree-2 *column* tensor index.
128	///
129	/// Prefer [`Self::dual`] over `self.r_dual().c_dual()` to half negations.
130	#[must_use]
131	fn c_dual(&self) -> Self;
132
133	/// Raises/Lowers *all* of its degree-1/degree-2 tensor indices *in-place*.
134	///
135	/// Negates the appropriate components avoiding matrix multiplication.
136	fn dual_mut(&mut self);
137
138	/// Raises/Lowers its degree-1/degree-2 *row* tensor index *in-place*.
139	///
140	/// Prefer [`Self::dual`] over `self.r_dual_mut().c_dual_mut()` to half
141	/// negations.
142	fn r_dual_mut(&mut self);
143
144	/// Raises/Lowers its degree-1/degree-2 *column* tensor index *in-place*.
145	///
146	/// Prefer [`Self::dual`] over `self.r_dual_mut().c_dual_mut()` to half negations.
147	fn c_dual_mut(&mut self);
148
149	/// Lorentzian matrix multiplication of degree-1/degree-2 tensors.
150	///
151	/// Equals `self.c_dual() * rhs`, the metric contraction of its *column* index with `rhs`'s
152	/// *row* index.
153	#[must_use]
154	fn contr<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> OMatrix<T, R, C2>
155	where
156		R2: Dim,
157		C2: Dim,
158		SB: Storage<T, R2, C2>,
159		ShapeConstraint: AreMultipliable<R, C, R2, C2>,
160		DefaultAllocator: Allocator<R, C2>;
161
162	/// Same as [`Self::contr`] but with transposed tensor indices.
163	///
164	/// Equals `self.r_dual().tr_mul(rhs)`, the metric contraction of its *transposed row* index
165	/// with `rhs`'s *row* index.
166	#[must_use]
167	fn tr_contr<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> OMatrix<T, C, C2>
168	where
169		R2: Dim,
170		C2: Dim,
171		SB: Storage<T, R2, C2>,
172		ShapeConstraint: SameNumberOfRows<R, R2>,
173		DefaultAllocator: Allocator<C, C2>;
174
175	/// Lorentzian inner product of degree-1/degree-2 tensors.
176	///
177	/// Equals `self.dual().dot(rhs)`, the metric contraction of:
178	///
179	///   * one index for degree-1 tensors, and
180	///   * two indices for degree-2 tensors.
181	///
182	/// Also known as:
183	///
184	///   * Minkowski inner product,
185	///   * relativistic dot product,
186	///   * Lorentz scalar, invariant under Lorentz transformations, or
187	///   * spacetime interval between two events, see [`Self::interval`].
188	#[must_use]
189	fn scalar<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> T
190	where
191		R2: Dim,
192		C2: Dim,
193		SB: Storage<T, R2, C2>,
194		ShapeConstraint: DimEq<R, R2> + DimEq<C, C2>;
195
196	/// Same as [`Self::scalar`] but with transposed tensor indices.
197	///
198	/// Equals `self.dual().tr_dot(rhs)`.
199	#[must_use]
200	fn tr_scalar<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> T
201	where
202		R2: Dim,
203		C2: Dim,
204		SB: Storage<T, R2, C2>,
205		ShapeConstraint: DimEq<C, R2> + DimEq<R, C2>;
206
207	/// Lorentzian norm of timelike degree-1/degree-2 tensors.
208	///
209	/// Equals `self.scalar(self).neg().sqrt()`.
210	///
211	/// If spacelike, returns *NaN* or panics if `N` doesn't support it.
212	#[must_use]
213	fn timelike_norm(&self) -> T;
214
215	/// Lorentzian norm of spacelike degree-1/degree-2 tensors.
216	///
217	/// Equals `self.scalar(self).sqrt()`.
218	///
219	/// If timelike, returns *NaN* or panics if `N` doesn't support it.
220	#[must_use]
221	fn spacelike_norm(&self) -> T;
222
223	/// Spacetime interval between two events and region of `self`'s light cone.
224	///
225	/// Same as [`Self::interval_fn`] but with [`AbsDiffEq::default_epsilon`] as in:
226	///
227	///   * `is_present = |time| abs_diff_eq!(time, T::zero())`, and
228	///   * `is_lightlike = |interval| abs_diff_eq!(interval, T::zero())`.
229	#[must_use]
230	fn interval(&self, rhs: &Self) -> (T, LightCone)
231	where
232		T: AbsDiffEq,
233		ShapeConstraint: DimEq<U1, C>;
234
235	/// Spacetime interval between two events and region of `self`'s light cone.
236	///
237	/// Equals `(rhs - self).scalar(&(rhs - self))` where `self` is subtracted from `rhs` to depict
238	/// `rhs` in `self`'s light cone.
239	///
240	/// Requires you to approximate when `N` equals `T::zero()` via:
241	///
242	///   * `is_present` for the time component of `rhs - self`, and
243	///   * `is_lightlike` for the interval.
244	///
245	/// Their signs are only evaluated in the `false` branches of `is_present` and `is_lightlike`.
246	///
247	/// See [`Self::interval`] for using defaults and [`approx`] for further details.
248	#[must_use]
249	fn interval_fn<P, L>(&self, rhs: &Self, is_present: P, is_lightlike: L) -> (T, LightCone)
250	where
251		ShapeConstraint: DimEq<U1, C>,
252		P: Fn(&T) -> bool,
253		L: Fn(&T) -> bool;
254
255	/// $
256	/// \gdef \uk {\hat u \cdot \vec K}
257	/// \gdef \Lmu {\Lambda^{\mu\'}\_{\phantom {\mu\'} \mu}}
258	/// \gdef \Lnu {(\Lambda^T)\_\nu^{\phantom \nu \nu\'}}
259	/// $
260	/// Lorentz transformation $\Lmu(\hat u, \zeta)$ boosting degree-1/degree-2 tensors to inertial
261	/// `frame` of reference.
262	///
263	/// $$
264	/// \Lmu(\hat u, \zeta) = I - \sinh \zeta (\uk) + (\cosh \zeta - 1) (\uk)^2
265	/// $$
266	///
267	/// Where $\uk$ is the generator of the boost along $\hat{u}$ with its spatial componentsi
268	/// $(u^1, \dots, u^{n-1})$:
269	///
270	/// $$
271	/// \uk = \begin{pmatrix}
272	///    0    &   u^1  &  \dots & u^{n-1} \\\\
273	///   u^1   &    0   &  \dots &    0    \\\\
274	/// \vdots  & \vdots & \ddots & \vdots  \\\\
275	/// u^{n-1} &    0   &  \dots &    0    \end{pmatrix}
276	/// $$
277	///
278	/// Boosts degree-1 tensors by multiplying it from the left:
279	///
280	/// $$
281	/// x^{\mu\'} = \Lmu x^\mu
282	/// $$
283	///
284	/// Boosts degree-2 tensors by multiplying it from the left and its transpose (symmetric for
285	/// pure boosts) from the right:
286	///
287	/// $$
288	/// F^{\mu\' \nu\'} = \Lmu F^{\mu \nu} \Lnu
289	/// $$
290	///
291	/// ```
292	/// use approx::assert_ulps_eq;
293	/// use nalgebra::{Matrix4, Vector3, Vector4};
294	/// use nalgebra_spacetime::{Frame4, Lorentzian};
295	///
296	/// let event = Vector4::new_random();
297	/// let frame = Frame4::from_beta(Vector3::new(0.3, -0.4, 0.6));
298	/// let boost = Matrix4::new_boost(&frame);
299	/// assert_ulps_eq!(boost * event, event.boost(&frame), epsilon = 1e-14);
300	/// ```
301	#[must_use]
302	fn new_boost<D>(frame: &OFrame<T, D>) -> Self
303	where
304		D: DimNameSub<U1>,
305		ShapeConstraint: AreMultipliable<R, C, R, C> + DimEq<R, D>,
306		DefaultAllocator: Allocator<DimNameDiff<D, U1>>;
307
308	/// Boosts this degree-1 tensor $x^\mu$ to inertial `frame` of reference along $\hat u$ with
309	/// $\zeta$.
310	///
311	/// ```
312	/// use approx::assert_ulps_eq;
313	/// use nalgebra::{Vector3, Vector4};
314	/// use nalgebra_spacetime::{Frame4, Lorentzian};
315	///
316	/// let muon_lifetime_at_rest = Vector4::new(2.2e-6, 0.0, 0.0, 0.0);
317	/// let muon_frame = Frame4::from_axis_beta(Vector3::z_axis(), 0.9952);
318	/// let muon_lifetime = muon_lifetime_at_rest.boost(&muon_frame);
319	/// let time_dilation_factor = muon_lifetime[0] / muon_lifetime_at_rest[0];
320	/// assert_ulps_eq!(time_dilation_factor, 10.218, epsilon = 1e-3);
321	/// ```
322	///
323	/// See `boost_mut()` for further details.
324	#[must_use]
325	fn boost<D>(&self, frame: &OFrame<T, D>) -> Self
326	where
327		R: DimNameSub<U1>,
328		D: DimNameSub<U1>,
329		ShapeConstraint: SameNumberOfRows<R, D>
330			+ SameNumberOfColumns<C, U1>
331			+ DimEq<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>
332			+ SameNumberOfRows<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>,
333		DefaultAllocator: Allocator<DimNameDiff<D, U1>>;
334
335	/// $
336	/// \gdef \xu {(\vec x \cdot \hat u)}
337	/// $
338	/// Boosts this degree-1 tensor $x^\mu$ to inertial `frame` of reference along $\hat u$ with
339	/// $\zeta$ *in-place*.
340	///
341	/// $$
342	/// \begin{pmatrix}
343	/// x^0 \\\\
344	/// \vec x
345	/// \end{pmatrix}\' = \begin{pmatrix}
346	/// x^0 \cosh \zeta - \xu \sinh \zeta \\\\
347	/// \vec x + (\xu (\cosh \zeta - 1) - x^0 \sinh \zeta) \hat u
348	/// \end{pmatrix}
349	/// $$
350	///
351	/// ```
352	/// use approx::assert_ulps_eq;
353	/// use nalgebra::Vector4;
354	/// use nalgebra_spacetime::Lorentzian;
355	///
356	/// // Arbitrary timelike four-momentum.
357	/// let mut momentum = Vector4::new(24.3, 5.22, 16.8, 9.35);
358	///
359	/// // Rest mass.
360	/// let mass = momentum.timelike_norm();
361	/// // Four-momentum in center-of-momentum frame.
362	/// let mass_at_rest = Vector4::new(mass, 0.0, 0.0, 0.0);
363	///
364	/// // Rest mass is ratio of four-momentum to four-velocity.
365	/// let velocity = momentum / mass;
366	/// // Four-momentum boosted to center-of-momentum frame.
367	/// momentum.boost_mut(&velocity.frame());
368	///
369	/// // Verify boosting four-momentum to center-of-momentum frame.
370	/// assert_ulps_eq!(momentum, mass_at_rest, epsilon = 1e-14);
371	/// ```
372	fn boost_mut<D>(&mut self, frame: &OFrame<T, D>)
373	where
374		R: DimNameSub<U1>,
375		D: DimNameSub<U1>,
376		ShapeConstraint: SameNumberOfRows<R, D>
377			+ SameNumberOfColumns<C, U1>
378			+ DimEq<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>
379			+ SameNumberOfRows<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>,
380		DefaultAllocator: Allocator<DimNameDiff<D, U1>>;
381
382	/// Velocity $u^\mu$ of inertial `frame` of reference.
383	#[must_use]
384	fn new_velocity<D>(frame: &OFrame<T, D>) -> OVector<T, D>
385	where
386		D: DimNameSub<U1>,
387		ShapeConstraint: SameNumberOfRows<R, D> + SameNumberOfColumns<C, U1>,
388		DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>;
389
390	/// Inertial frame of reference of this velocity $u^\mu$.
391	#[must_use]
392	fn frame(&self) -> OFrame<T, R>
393	where
394		R: DimNameSub<U1>,
395		ShapeConstraint: SameNumberOfColumns<C, U1>,
396		DefaultAllocator: Allocator<DimNameDiff<R, U1>>;
397
398	/// From `temporal` and `spatial` spacetime split.
399	#[must_use]
400	fn from_split<D>(temporal: &T, spatial: &OVector<T, DimNameDiff<D, U1>>) -> OVector<T, D>
401	where
402		D: DimNameSub<U1>,
403		ShapeConstraint: SameNumberOfRows<R, D> + SameNumberOfColumns<C, U1>,
404		DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
405		<DefaultAllocator as Allocator<D, U1>>::Buffer<T>:
406			StorageMut<T, D, U1, RStride = U1, CStride = D>;
407
408	/// Spacetime split into [`Self::temporal`] and [`Self::spatial`].
409	#[must_use]
410	fn split(&self) -> (&T, MatrixView<T, DimNameDiff<R, U1>, C, U1, R>)
411	where
412		R: DimNameSub<U1>,
413		ShapeConstraint: DimEq<U1, C>,
414		DefaultAllocator: Allocator<R, C>,
415		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
416			Storage<T, R, C, RStride = U1, CStride = R>;
417
418	/// Mutable spacetime split into [`Self::temporal_mut`] and
419	/// [`Self::spatial_mut`].
420	///
421	/// ```
422	/// use approx::assert_ulps_eq;
423	/// use nalgebra::Vector4;
424	/// use nalgebra_spacetime::Lorentzian;
425	///
426	/// let mut spacetime = Vector4::new(1.0, 2.0, 3.0, 4.0);
427	/// let (temporal, mut spatial) = spacetime.split_mut();
428	/// *temporal += 1.0;
429	/// spatial[0] += 2.0;
430	/// spatial[1] += 3.0;
431	/// spatial[2] += 4.0;
432	/// assert_ulps_eq!(spacetime, Vector4::new(2.0, 4.0, 6.0, 8.0));
433	/// ```
434	fn split_mut(&mut self) -> (&mut T, MatrixViewMut<T, DimNameDiff<R, U1>, C>)
435	where
436		R: DimNameSub<U1>,
437		ShapeConstraint: DimEq<U1, C>,
438		DefaultAllocator: Allocator<R, C>,
439		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
440			Storage<T, R, C, RStride = U1, CStride = R>;
441
442	/// Temporal component.
443	#[must_use]
444	fn temporal(&self) -> &T
445	where
446		R: DimNameSub<U1>,
447		ShapeConstraint: DimEq<U1, C>;
448
449	/// Mutable temporal component.
450	fn temporal_mut(&mut self) -> &mut T
451	where
452		R: DimNameSub<U1>,
453		ShapeConstraint: DimEq<U1, C>;
454
455	/// Spatial components.
456	#[must_use]
457	fn spatial(&self) -> MatrixView<T, DimNameDiff<R, U1>, C, U1, R>
458	where
459		R: DimNameSub<U1>,
460		ShapeConstraint: DimEq<U1, C>,
461		DefaultAllocator: Allocator<R, C>,
462		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
463			Storage<T, R, C, RStride = U1, CStride = R>;
464
465	/// Mutable spatial components.
466	fn spatial_mut(&mut self) -> MatrixViewMut<T, DimNameDiff<R, U1>, C, U1, R>
467	where
468		R: DimNameSub<U1>,
469		ShapeConstraint: DimEq<U1, C>,
470		DefaultAllocator: Allocator<R, C>,
471		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
472			StorageMut<T, R, C, RStride = U1, CStride = R>;
473}
474
475impl<T, R, C> Lorentzian<T, R, C> for OMatrix<T, R, C>
476where
477	T: RealField,
478	R: DimName + DimNameSub<U1>,
479	C: DimName + DimNameSub<U1>,
480	DefaultAllocator: Allocator<R, C>,
481{
482	#[inline]
483	fn metric() -> Self
484	where
485		ShapeConstraint: SameDimension<R, C>,
486	{
487		let mut m = Self::identity();
488		neg(m.get_mut((0, 0)).unwrap());
489		m
490	}
491
492	#[inline]
493	fn dual(&self) -> Self {
494		let mut m = self.clone_owned();
495		m.dual_mut();
496		m
497	}
498
499	#[inline]
500	fn r_dual(&self) -> Self {
501		let mut m = self.clone_owned();
502		m.r_dual_mut();
503		m
504	}
505
506	#[inline]
507	fn c_dual(&self) -> Self {
508		let mut m = self.clone_owned();
509		m.c_dual_mut();
510		m
511	}
512
513	#[inline]
514	fn dual_mut(&mut self) {
515		if R::is::<U1>() || C::is::<U1>() {
516			neg(self.get_mut(0).unwrap());
517		} else if R::is::<C>() {
518			for i in 1..R::dim() {
519				neg(self.get_mut((i, 0)).unwrap());
520				neg(self.get_mut((0, i)).unwrap());
521			}
522		} else {
523			for r in 1..R::dim() {
524				neg(self.get_mut((r, 0)).unwrap());
525			}
526			for c in 1..C::dim() {
527				neg(self.get_mut((0, c)).unwrap());
528			}
529		}
530	}
531
532	#[inline]
533	fn r_dual_mut(&mut self) {
534		for c in 0..C::dim() {
535			neg(self.get_mut((0, c)).unwrap());
536		}
537	}
538
539	#[inline]
540	fn c_dual_mut(&mut self) {
541		for r in 0..R::dim() {
542			neg(self.get_mut((r, 0)).unwrap());
543		}
544	}
545
546	#[inline]
547	fn contr<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> OMatrix<T, R, C2>
548	where
549		R2: Dim,
550		C2: Dim,
551		SB: Storage<T, R2, C2>,
552		ShapeConstraint: AreMultipliable<R, C, R2, C2>,
553		DefaultAllocator: Allocator<R, C2>,
554	{
555		self.c_dual() * rhs
556	}
557
558	#[inline]
559	fn tr_contr<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> OMatrix<T, C, C2>
560	where
561		R2: Dim,
562		C2: Dim,
563		SB: Storage<T, R2, C2>,
564		ShapeConstraint: SameNumberOfRows<R, R2>,
565		DefaultAllocator: Allocator<C, C2>,
566	{
567		self.r_dual().tr_mul(rhs)
568	}
569
570	#[inline]
571	fn scalar<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> T
572	where
573		R2: Dim,
574		C2: Dim,
575		SB: Storage<T, R2, C2>,
576		ShapeConstraint: DimEq<R, R2> + DimEq<C, C2>,
577	{
578		self.dual().dot(rhs)
579	}
580
581	#[inline]
582	fn tr_scalar<R2, C2, SB>(&self, rhs: &Matrix<T, R2, C2, SB>) -> T
583	where
584		R2: Dim,
585		C2: Dim,
586		SB: Storage<T, R2, C2>,
587		ShapeConstraint: DimEq<C, R2> + DimEq<R, C2>,
588	{
589		self.dual().tr_dot(rhs)
590	}
591
592	#[inline]
593	fn timelike_norm(&self) -> T {
594		self.scalar(self).neg().sqrt()
595	}
596
597	#[inline]
598	fn spacelike_norm(&self) -> T {
599		self.scalar(self).sqrt()
600	}
601
602	#[inline]
603	fn interval(&self, rhs: &Self) -> (T, LightCone)
604	where
605		T: AbsDiffEq,
606		ShapeConstraint: DimEq<U1, C>,
607	{
608		self.interval_fn(
609			rhs,
610			|time| abs_diff_eq!(time, &T::zero()),
611			|interval| abs_diff_eq!(interval, &T::zero()),
612		)
613	}
614
615	fn interval_fn<P, L>(&self, rhs: &Self, is_present: P, is_lightlike: L) -> (T, LightCone)
616	where
617		ShapeConstraint: DimEq<U1, C>,
618		P: Fn(&T) -> bool,
619		L: Fn(&T) -> bool,
620	{
621		let time = self[0].clone();
622		let difference = rhs - self;
623		let interval = difference.scalar(&difference);
624		let light_cone = if is_lightlike(&interval) {
625			if is_present(&time) {
626				LightCone::Origin
627			} else if time.is_sign_positive() {
628				LightCone::LightlikeFuture
629			} else {
630				LightCone::LightlikePast
631			}
632		} else if interval.is_sign_positive() || is_present(&time) {
633			LightCone::Spacelike
634		} else if time.is_sign_positive() {
635			LightCone::TimelikeFuture
636		} else {
637			LightCone::TimelikePast
638		};
639		(interval, light_cone)
640	}
641
642	fn new_boost<D>(frame: &OFrame<T, D>) -> Self
643	where
644		D: DimNameSub<U1>,
645		ShapeConstraint: AreMultipliable<R, C, R, C> + DimEq<R, D>,
646		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
647	{
648		let OFrame {
649			zeta_cosh,
650			zeta_sinh,
651			axis,
652		} = frame;
653		let mut b = Self::zeros();
654		for (i, u) in axis.iter().enumerate() {
655			b[(i + 1, 0)] = u.clone();
656			b[(0, i + 1)] = u.clone();
657		}
658		let uk = b.clone_owned();
659		b.gemm(zeta_cosh.clone() - T::one(), &uk, &uk, -zeta_sinh.clone());
660		for i in 0..D::dim() {
661			b[(i, i)] += T::one();
662		}
663		b
664	}
665
666	#[inline]
667	fn boost<D>(&self, frame: &OFrame<T, D>) -> Self
668	where
669		R: DimNameSub<U1>,
670		D: DimNameSub<U1>,
671		ShapeConstraint: SameNumberOfRows<R, D>
672			+ SameNumberOfColumns<C, U1>
673			+ DimEq<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>
674			+ SameNumberOfRows<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>,
675		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
676	{
677		let mut v = self.clone_owned();
678		v.boost_mut(frame);
679		v
680	}
681
682	fn boost_mut<D>(&mut self, frame: &OFrame<T, D>)
683	where
684		R: DimNameSub<U1>,
685		D: DimNameSub<U1>,
686		ShapeConstraint: SameNumberOfRows<R, D>
687			+ SameNumberOfColumns<C, U1>
688			+ DimEq<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>
689			+ SameNumberOfRows<<R as DimNameSub<U1>>::Output, <D as DimNameSub<U1>>::Output>,
690		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
691	{
692		let OFrame {
693			zeta_cosh,
694			zeta_sinh,
695			axis,
696		} = frame;
697		let u = axis.as_ref();
698		let a = self[0].clone();
699		let (rows, _cols) = self.shape_generic();
700		let zu = self.rows_generic(1, rows.sub(U1)).dot(u);
701		self[0] = zeta_cosh.clone() * a.clone() - zeta_sinh.clone() * zu.clone();
702		let mut z = self.rows_generic_mut(1, rows.sub(U1));
703		z += u * ((zeta_cosh.clone() - T::one()) * zu - zeta_sinh.clone() * a);
704	}
705
706	#[inline]
707	fn new_velocity<D>(frame: &OFrame<T, D>) -> OVector<T, D>
708	where
709		D: DimNameSub<U1>,
710		ShapeConstraint: SameNumberOfRows<R, D> + SameNumberOfColumns<C, U1>,
711		DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
712	{
713		frame.velocity()
714	}
715
716	#[inline]
717	fn frame(&self) -> OFrame<T, R>
718	where
719		R: DimNameSub<U1>,
720		ShapeConstraint: SameNumberOfColumns<C, U1>,
721		DefaultAllocator: Allocator<DimNameDiff<R, U1>>,
722	{
723		OFrame::<T, R>::from_velocity(self)
724	}
725
726	#[inline]
727	fn from_split<D>(temporal: &T, spatial: &OVector<T, DimNameDiff<D, U1>>) -> OVector<T, D>
728	where
729		D: DimNameSub<U1>,
730		ShapeConstraint: SameNumberOfRows<R, D> + SameNumberOfColumns<C, U1>,
731		DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
732		<DefaultAllocator as Allocator<D, U1>>::Buffer<T>:
733			StorageMut<T, D, U1, RStride = U1, CStride = D>,
734	{
735		let mut v = OVector::<T, D>::zeros();
736		*v.temporal_mut() = temporal.clone();
737		v.spatial_mut().copy_from(spatial);
738		v
739	}
740
741	#[inline]
742	fn split(&self) -> (&T, MatrixView<T, DimNameDiff<R, U1>, C, U1, R>)
743	where
744		R: DimNameSub<U1>,
745		ShapeConstraint: DimEq<U1, C>,
746		DefaultAllocator: Allocator<R, C>,
747		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
748			Storage<T, R, C, RStride = U1, CStride = R>,
749	{
750		(self.temporal(), self.spatial())
751	}
752
753	#[inline]
754	fn split_mut(&mut self) -> (&mut T, MatrixViewMut<T, DimNameDiff<R, U1>, C>)
755	where
756		R: DimNameSub<U1>,
757		ShapeConstraint: DimEq<U1, C>,
758		DefaultAllocator: Allocator<R, C>,
759		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
760			Storage<T, R, C, RStride = U1, CStride = R>,
761	{
762		let (temporal, spatial) = self.as_mut_slice().split_at_mut(1);
763		(
764			temporal.get_mut(0).unwrap(),
765			MatrixViewMut::<T, DimNameDiff<R, U1>, C>::from_slice(spatial),
766		)
767	}
768
769	#[inline]
770	fn temporal(&self) -> &T
771	where
772		R: DimNameSub<U1>,
773		ShapeConstraint: DimEq<U1, C>,
774	{
775		self.get(0).unwrap()
776	}
777
778	#[inline]
779	fn temporal_mut(&mut self) -> &mut T
780	where
781		R: DimNameSub<U1>,
782		ShapeConstraint: DimEq<U1, C>,
783	{
784		self.get_mut(0).unwrap()
785	}
786
787	#[inline]
788	fn spatial(&self) -> MatrixView<T, DimNameDiff<R, U1>, C, U1, R>
789	where
790		R: DimNameSub<U1>,
791		ShapeConstraint: DimEq<U1, C>,
792		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
793			RawStorage<T, R, C, RStride = U1, CStride = R>,
794	{
795		let (rows, _cols) = self.shape_generic();
796		self.rows_generic(1, rows.sub(U1))
797	}
798
799	#[inline]
800	fn spatial_mut(&mut self) -> MatrixViewMut<T, DimNameDiff<R, U1>, C, U1, R>
801	where
802		R: DimNameSub<U1>,
803		ShapeConstraint: DimEq<U1, C>,
804		<DefaultAllocator as Allocator<R, C>>::Buffer<T>:
805			RawStorageMut<T, R, C, RStride = U1, CStride = R>,
806	{
807		let (rows, _cols) = self.shape_generic();
808		self.rows_generic_mut(1, rows.sub(U1))
809	}
810}
811
812/// Spacetime regions regarding an event's light cone.
813#[derive(Debug, PartialEq, Eq, Clone, Copy)]
814pub enum LightCone {
815	/// *Interior* events of the future-directed light cone.
816	TimelikeFuture,
817	/// Events *on* the future-directed light cone itself.
818	LightlikeFuture,
819	/// Trivial *zero-vector* difference of two coinciding events.
820	Origin,
821	/// Events *on* the past-directed light cone itself.
822	LightlikePast,
823	/// *Interior* events of the past-directed light cone.
824	TimelikePast,
825	/// Events *elsewhere*.
826	Spacelike,
827}
828
829/// Inertial frame of reference in $2$-dimensional Lorentzian space
830/// $\R^{-,+} = \R^{1,1}$.
831pub type Frame2<T> = OFrame<T, U2>;
832/// Inertial frame of reference in $3$-dimensional Lorentzian space
833/// $\R^{-,+} = \R^{1,2}$.
834pub type Frame3<T> = OFrame<T, U3>;
835/// Inertial frame of reference in $4$-dimensional Lorentzian space
836/// $\R^{-,+} = \R^{1,3}$.
837pub type Frame4<T> = OFrame<T, U4>;
838/// Inertial frame of reference in $5$-dimensional Lorentzian space
839/// $\R^{-,+} = \R^{1,4}$.
840pub type Frame5<T> = OFrame<T, U5>;
841/// Inertial frame of reference in $6$-dimensional Lorentzian space
842/// $\R^{-,+} = \R^{1,5}$.
843pub type Frame6<T> = OFrame<T, U6>;
844
845/// Inertial frame of reference in $n$-dimensional Lorentzian space $\R^{-,+} = \R^{1,n-1}$.
846///
847/// Holds a statically sized direction axis $\hat u \in \R^{n-1}$ and two boost parameters
848/// precomputed from either velocity $u^\mu$, rapidity $\vec \zeta$, or velocity ratio $\vec \beta$
849/// whether using [`Self::from_velocity`], [`Self::from_zeta`], or [`Self::from_beta`]:
850///
851/// $$
852/// \cosh \zeta = \gamma
853/// $$
854///
855/// $$
856/// \sinh \zeta = \beta \gamma
857/// $$
858///
859/// Where $\gamma$ is the Lorentz factor.
860#[derive(Debug, PartialEq, Eq, Clone)]
861pub struct OFrame<T, D>
862where
863	T: Scalar,
864	D: DimNameSub<U1>,
865	DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
866{
867	zeta_cosh: T,
868	zeta_sinh: T,
869	axis: Unit<OVector<T, DimNameDiff<D, U1>>>,
870}
871
872impl<T, D> OFrame<T, D>
873where
874	T: RealField,
875	D: DimNameSub<U1>,
876	DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
877{
878	/// Inertial frame of reference with velocity $u^\mu$.
879	#[must_use]
880	pub fn from_velocity<R, C>(u: &OMatrix<T, R, C>) -> Self
881	where
882		R: DimNameSub<U1>,
883		C: Dim,
884		ShapeConstraint: SameNumberOfRows<R, D> + SameNumberOfColumns<C, U1>,
885		DefaultAllocator: Allocator<R, C>,
886	{
887		let mut scaled_axis = OVector::zeros();
888		let zeta_cosh = u[0].clone();
889		let (rows, _cols) = u.shape_generic();
890		scaled_axis
891			.iter_mut()
892			.zip(u.rows_generic(1, rows.sub(U1)).iter())
893			.for_each(|(scaled_axis, u)| *scaled_axis = u.clone());
894		let (axis, zeta_sinh) = Unit::new_and_get(scaled_axis);
895		Self {
896			zeta_cosh,
897			zeta_sinh,
898			axis,
899		}
900	}
901
902	/// Inertial frame of reference with rapidity $\vec \zeta$.
903	#[must_use]
904	#[inline]
905	pub fn from_zeta(scaled_axis: OVector<T, DimNameDiff<D, U1>>) -> Self {
906		let (axis, zeta) = Unit::new_and_get(scaled_axis);
907		Self::from_axis_zeta(axis, zeta)
908	}
909
910	/// Inertial frame of reference along $\hat u$ with rapidity $\zeta$.
911	#[must_use]
912	#[inline]
913	pub fn from_axis_zeta(axis: Unit<OVector<T, DimNameDiff<D, U1>>>, zeta: T) -> Self {
914		Self {
915			zeta_cosh: zeta.clone().cosh(),
916			zeta_sinh: zeta.sinh(),
917			axis,
918		}
919	}
920
921	/// Inertial frame of reference with velocity ratio $\vec \beta$.
922	#[must_use]
923	#[inline]
924	pub fn from_beta(scaled_axis: OVector<T, DimNameDiff<D, U1>>) -> Self {
925		let (axis, beta) = Unit::new_and_get(scaled_axis);
926		Self::from_axis_beta(axis, beta)
927	}
928
929	/// Inertial frame of reference along $\hat u$ with velocity ratio $\beta$.
930	#[must_use]
931	#[inline]
932	pub fn from_axis_beta(axis: Unit<OVector<T, DimNameDiff<D, U1>>>, beta: T) -> Self {
933		debug_assert!(
934			-T::one() < beta && beta < T::one(),
935			"Velocity ratio `beta` is out of range (-1, +1)"
936		);
937		let gamma = T::one() / (T::one() - beta.clone() * beta.clone()).sqrt();
938		Self {
939			zeta_cosh: gamma.clone(),
940			zeta_sinh: beta * gamma,
941			axis,
942		}
943	}
944
945	/// Velocity $u^\mu$.
946	#[must_use]
947	pub fn velocity(&self) -> OVector<T, D>
948	where
949		DefaultAllocator: Allocator<D>,
950	{
951		let mut u = OVector::<T, D>::zeros();
952		u[0] = self.gamma();
953		let (rows, _cols) = u.shape_generic();
954		u.rows_generic_mut(1, rows.sub(U1))
955			.iter_mut()
956			.zip(self.axis.iter())
957			.for_each(|(u, axis)| *u = self.beta_gamma() * axis.clone());
958		u
959	}
960
961	/// Direction $\hat u$.
962	#[must_use]
963	#[inline]
964	pub fn axis(&self) -> Unit<OVector<T, DimNameDiff<D, U1>>> {
965		self.axis.clone()
966	}
967
968	/// Rapidity $\zeta$.
969	#[must_use]
970	#[inline]
971	pub fn zeta(&self) -> T {
972		self.beta().atanh()
973	}
974
975	/// Velocity ratio $\beta$.
976	#[must_use]
977	#[inline]
978	pub fn beta(&self) -> T {
979		self.beta_gamma() / self.gamma()
980	}
981
982	/// Lorentz factor $\gamma$.
983	#[must_use]
984	#[inline]
985	pub fn gamma(&self) -> T {
986		self.zeta_cosh.clone()
987	}
988
989	/// Product of velocity ratio $\beta$ and Lorentz factor $\gamma$.
990	#[must_use]
991	#[inline]
992	pub fn beta_gamma(&self) -> T {
993		self.zeta_sinh.clone()
994	}
995
996	/// Relativistic velocity addition `self`$\oplus$`frame`.
997	///
998	/// Equals `frame.velocity().boost(&-self.clone()).frame()`.
999	#[must_use]
1000	#[inline]
1001	pub fn compose(&self, frame: &Self) -> Self
1002	where
1003		DefaultAllocator: Allocator<D>,
1004	{
1005		frame.velocity().boost(&-self.clone()).frame()
1006	}
1007
1008	/// Wigner rotation angle $\epsilon$ of the boost composition `self`$\oplus$`frame`.
1009	///
1010	/// The angle between the forward and backward composition:
1011	///
1012	/// $$
1013	/// \epsilon = \angle (\vec \beta_u \oplus \vec \beta_v, \vec \beta_v \oplus \vec \beta_u)
1014	/// $$
1015	///
1016	/// See [`Self::rotation`] for further details.
1017	#[must_use]
1018	pub fn angle(&self, frame: &Self) -> T
1019	where
1020		DefaultAllocator: Allocator<D>,
1021	{
1022		let (u, v) = (self, frame);
1023		let ucv = u.compose(v).axis();
1024		let vcu = v.compose(u).axis();
1025		ucv.dot(&vcu).clamp(-T::one(), T::one()).acos()
1026	}
1027
1028	/// $
1029	/// \gdef \Bu {B^{\mu\'}\_{\phantom {\mu\'} \mu} (\vec \beta_u)}
1030	/// \gdef \Bv {B^{\mu\'\'}\_{\phantom {\mu\'\'} \mu\'} (\vec \beta_v)}
1031	/// \gdef \Puv {u \oplus v}
1032	/// \gdef \Buv {B^{\mu\'}\_{\phantom {\mu\'} \mu} (\vec \beta_{\Puv})}
1033	/// \gdef \Ruv {R^{\mu\'\'}\_{\phantom {\mu\'\'} \mu\'} (\epsilon)}
1034	/// \gdef \R {R (\epsilon)}
1035	/// \gdef \Kuv {K(\epsilon)}
1036	/// \gdef \Luv {\Lambda^{\mu\'\'}\_{\phantom {\mu\'\'} \mu} (\vec \beta_{\Puv})}
1037	/// $
1038	/// Wigner rotation matrix $R(\widehat {\vec \beta_u \wedge \vec \beta_v}, \epsilon)$ of the
1039	/// boost composition `self`$\oplus$`frame`.
1040	///
1041	/// The composition of two pure boosts, $\Bu$ to `self` followed by $\Bv$ to `frame`, results in
1042	/// a composition of a pure boost $\Buv$ and a *non-vanishing* spatial rotation $\Ruv$ for
1043	/// *non-collinear* boosts:
1044	///
1045	/// $$
1046	/// \Luv = \Ruv \Buv = \Bv \Bu
1047	/// $$
1048	///
1049	/// The returned homogeneous rotation matrix
1050	///
1051	/// $$
1052	/// \R = \begin{pmatrix}
1053	///      1      & \vec{0}^T \\\\
1054	///   \vec{0}   &   \Kuv    \end{pmatrix}
1055	/// $$
1056	///
1057	/// embeds the spatial rotation matrix
1058	///
1059	/// $$
1060	/// \Kuv = I + (\hat b\hat a^T - \hat a\hat b^T) + \frac{1}{1+\hat a\cdot\hat b} \left[
1061	///   (\hat a\cdot\hat b)(\hat b\hat a^T+\hat a\hat b^T) - (\hat a\hat a^T + \hat b\hat b^T)
1062	/// \right]
1063	/// $$
1064	///
1065	/// rotating in the plane spanned by the arc from
1066	/// $\hat a = \widehat{\vec\beta_u\oplus\vec\beta_v}$ to
1067	/// $\hat b = \widehat{\vec\beta_v\oplus\vec\beta_u}$ with the rotation angle
1068	/// $\epsilon = \arccos(\hat a\cdot\hat b)$.
1069	///
1070	/// See [`Self::axis_angle`] for $4$-dimensional specialization.
1071	///
1072	/// ```
1073	/// use approx::{assert_ulps_eq, assert_ulps_ne};
1074	/// use nalgebra::{Matrix4, Vector3};
1075	/// use nalgebra_spacetime::{Frame4, Lorentzian};
1076	///
1077	/// let u = Frame4::from_beta(Vector3::new(0.18, 0.73, 0.07));
1078	/// let v = Frame4::from_beta(Vector3::new(0.41, 0.14, 0.25));
1079	/// let ucv = u.compose(&v);
1080	/// let vcu = v.compose(&u);
1081	///
1082	/// let boost_u = Matrix4::new_boost(&u);
1083	/// let boost_v = Matrix4::new_boost(&v);
1084	/// let boost_ucv = Matrix4::new_boost(&ucv);
1085	/// let boost_vcu = Matrix4::new_boost(&vcu);
1086	///
1087	/// let (matrix_ucv, angle_ucv) = u.rotation(&v);
1088	///
1089	/// assert_ulps_eq!(angle_ucv, u.angle(&v));
1090	/// assert_ulps_ne!(boost_ucv, boost_v * boost_u);
1091	/// assert_ulps_ne!(boost_vcu, boost_u * boost_v);
1092	/// assert_ulps_eq!(matrix_ucv * boost_ucv, boost_v * boost_u);
1093	/// assert_ulps_eq!(boost_vcu * matrix_ucv, boost_v * boost_u);
1094	/// ```
1095	#[must_use]
1096	#[allow(clippy::similar_names, clippy::many_single_char_names)]
1097	pub fn rotation(&self, frame: &Self) -> (OMatrix<T, D, D>, T)
1098	where
1099		DefaultAllocator: Allocator<D>
1100			+ Allocator<U1, DimNameDiff<D, U1>>
1101			+ Allocator<D, D>
1102			+ Allocator<DimNameDiff<D, U1>, DimNameDiff<D, U1>>,
1103	{
1104		let (u, v) = (self, frame);
1105		let a = &u.compose(v).axis().into_inner();
1106		let b = &v.compose(u).axis().into_inner();
1107		let ab = a.dot(b);
1108		let d = T::one() + ab.clone();
1109		let [at, bt] = &[a.transpose(), b.transpose()];
1110		let [b_at, a_bt, a_at, b_bt] = &[b * at, a * bt, a * at, b * bt];
1111		let [k1, k2] = [b_at - a_bt, (b_at + a_bt) * ab.clone() - (a_at + b_bt)];
1112		let mut mat = OMatrix::<T, D, D>::identity();
1113		let (r, c) = mat.shape_generic();
1114		let mut rot = mat.generic_view_mut((1, 1), (r.sub(U1), c.sub(U1)));
1115		rot += k1 + k2 / d;
1116		(mat, ab.clamp(-T::one(), T::one()).acos())
1117	}
1118
1119	/// Wigner rotation axis $\widehat {\vec \beta_u \times \vec \beta_v}$ and angle $\epsilon$ of
1120	/// the boost composition `self`$\oplus$`frame`.
1121	///
1122	/// $$
1123	/// \epsilon = \arcsin \Bigg({
1124	///   1 + \gamma + \gamma_u + \gamma_v
1125	///   \over
1126	///   (1 + \gamma) (1 + \gamma_u) (1 + \gamma_v)
1127	/// } \gamma_u \gamma_v \|\vec \beta_u \times \vec \beta_v\| \Bigg)
1128	/// $$
1129	///
1130	/// $$
1131	/// \gamma = \gamma_u \gamma_v (1 + \vec \beta_u \cdot \vec \beta_v)
1132	/// $$
1133	///
1134	/// See [`Self::rotation`] for $n$-dimensional generalization.
1135	///
1136	/// ```
1137	/// use approx::{assert_abs_diff_ne, assert_ulps_eq};
1138	/// use nalgebra::Vector3;
1139	/// use nalgebra_spacetime::{Frame4, Lorentzian};
1140	///
1141	/// let u = Frame4::from_beta(Vector3::new(0.18, 0.73, 0.07));
1142	/// let v = Frame4::from_beta(Vector3::new(0.41, 0.14, 0.25));
1143	///
1144	/// let ucv = u.compose(&v).axis();
1145	/// let vcu = v.compose(&u).axis();
1146	///
1147	/// let (axis, angle) = u.axis_angle(&v);
1148	/// let axis = axis.into_inner();
1149	///
1150	/// assert_abs_diff_ne!(angle, 0.0, epsilon = 1e-15);
1151	/// assert_ulps_eq!(angle, ucv.angle(&vcu), epsilon = 1e-15);
1152	/// assert_ulps_eq!(axis, ucv.cross(&vcu).normalize(), epsilon = 1e-15);
1153	/// ```
1154	#[must_use]
1155	pub fn axis_angle(&self, frame: &Self) -> (Unit<OVector<T, DimNameDiff<D, U1>>>, T)
1156	where
1157		T: RealField,
1158		D: DimNameSub<U1>,
1159		ShapeConstraint: DimEq<D, U4>,
1160		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1161	{
1162		let (u, v) = (self, frame);
1163		let (axis, sin) = Unit::new_and_get(u.axis().cross(&v.axis()));
1164		let uv = u.axis().dot(&v.axis());
1165		let bg = u.beta_gamma() * v.beta_gamma();
1166		let ug = u.gamma();
1167		let vg = v.gamma();
1168		let cg = ug.clone() * vg.clone() + bg.clone() * uv;
1169		let sum = T::one() + cg.clone() + ug.clone() + vg.clone();
1170		let prod = (T::one() + cg) * (T::one() + ug) * (T::one() + vg);
1171		(axis, (sum / prod * bg * sin).asin())
1172	}
1173}
1174
1175impl<T, R, C> From<OMatrix<T, R, C>> for OFrame<T, R>
1176where
1177	T: RealField,
1178	R: DimNameSub<U1>,
1179	C: DimNameSub<U1>,
1180	ShapeConstraint: SameNumberOfColumns<C, U1>,
1181	DefaultAllocator: Allocator<R, C> + Allocator<DimNameDiff<R, U1>>,
1182{
1183	#[inline]
1184	fn from(u: OMatrix<T, R, C>) -> Self {
1185		u.frame()
1186	}
1187}
1188
1189impl<T, D> From<OFrame<T, D>> for OVector<T, D>
1190where
1191	T: RealField,
1192	D: DimNameSub<U1>,
1193	DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
1194{
1195	#[inline]
1196	fn from(frame: OFrame<T, D>) -> Self {
1197		frame.velocity()
1198	}
1199}
1200
1201impl<T, D> Neg for OFrame<T, D>
1202where
1203	T: RealField,
1204	D: DimNameSub<U1>,
1205	DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1206{
1207	type Output = Self;
1208
1209	#[inline]
1210	fn neg(mut self) -> Self::Output {
1211		self.axis = -self.axis;
1212		self
1213	}
1214}
1215
1216impl<T, D> Add for OFrame<T, D>
1217where
1218	T: RealField,
1219	D: DimNameSub<U1>,
1220	DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
1221{
1222	type Output = Self;
1223
1224	#[inline]
1225	fn add(self, rhs: Self) -> Self::Output {
1226		self.compose(&rhs)
1227	}
1228}
1229
1230impl<T, D> Sub for OFrame<T, D>
1231where
1232	T: RealField,
1233	D: DimNameSub<U1>,
1234	DefaultAllocator: Allocator<DimNameDiff<D, U1>> + Allocator<D>,
1235{
1236	type Output = Self;
1237
1238	#[inline]
1239	fn sub(self, rhs: Self) -> Self::Output {
1240		self.compose(&-rhs)
1241	}
1242}
1243
1244impl<T, D> Copy for OFrame<T, D>
1245where
1246	T: RealField + Copy,
1247	D: DimNameSub<U1>,
1248	DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1249	Owned<T, DimNameDiff<D, U1>>: Copy,
1250{
1251}
1252
1253/// Momentum in $n$-dimensional Lorentzian space $\R^{-,+} = \R^{1,n-1}$.
1254///
1255/// Assuming unit system with speed of light $c=1$ and rest mass $m$ as timelike norm in spacelike
1256/// sign convention as in:
1257///
1258/// $$
1259/// m^2=E^2-\vec {p}^2=-p_\mu p^\mu
1260/// $$
1261///
1262/// Where $p^\mu$ is the $n$-momentum with energy $E$ as temporal $p^0$ and momentum $\vec p$ as
1263/// spatial $p^i$ components:
1264///
1265/// $$
1266/// p^\mu = m u^\mu = m \begin{pmatrix}
1267///   \gamma \\\\ \gamma \vec \beta
1268/// \end{pmatrix} = \begin{pmatrix}
1269///   \gamma m = E \\\\ \gamma m \vec \beta = \vec p
1270/// \end{pmatrix}
1271/// $$
1272///
1273/// With $n$-velocity $u^\mu$, Lorentz factor $\gamma$, and velocity ratio $\vec \beta$.
1274#[repr(transparent)]
1275#[derive(Debug, PartialEq, Clone)]
1276pub struct OMomentum<T, D>
1277where
1278	T: Scalar,
1279	D: DimNameSub<U1>,
1280	DefaultAllocator: Allocator<D>,
1281{
1282	momentum: OVector<T, D>,
1283}
1284
1285impl<T, D> OMomentum<T, D>
1286where
1287	T: RealField,
1288	D: DimNameSub<U1>,
1289	DefaultAllocator: Allocator<D>,
1290{
1291	/// Momentum with spacetime [`Lorentzian::split`], `energy` $E$ and
1292	/// `momentum` $\vec p$.
1293	#[must_use]
1294	#[inline]
1295	pub fn from_split(energy: &T, momentum: &OVector<T, DimNameDiff<D, U1>>) -> Self
1296	where
1297		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1298		<DefaultAllocator as Allocator<D, U1>>::Buffer<T>:
1299			StorageMut<T, D, U1, RStride = U1, CStride = D>,
1300	{
1301		Self {
1302			momentum: OVector::<T, D>::from_split(energy, momentum),
1303		}
1304	}
1305
1306	/// Momentum $p^\mu=m u^\mu$ with rest `mass` $m$ at `velocity` $u^\mu$.
1307	#[must_use]
1308	#[inline]
1309	pub fn from_mass_at_velocity(mass: T, velocity: OVector<T, D>) -> Self
1310	where
1311		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1312	{
1313		Self {
1314			momentum: velocity * mass,
1315		}
1316	}
1317
1318	/// Momentum $p^\mu$ with rest `mass` $m$ in `frame`.
1319	///
1320	/// Equals `frame.velocity() * mass`.
1321	#[must_use]
1322	#[inline]
1323	pub fn from_mass_in_frame(mass: T, frame: &OFrame<T, D>) -> Self
1324	where
1325		DefaultAllocator: Allocator<DimNameDiff<D, U1>>,
1326	{
1327		Self::from_mass_at_velocity(mass, frame.velocity())
1328	}
1329
1330	/// Momentum $p^\mu$ with rest `mass` $m$ in center-of-momentum frame.
1331	#[must_use]
1332	#[inline]
1333	pub fn from_mass_at_rest(mass: T) -> Self {
1334		let mut momentum = OVector::<T, D>::zeros();
1335		*momentum.temporal_mut() = mass;
1336		Self { momentum }
1337	}
1338
1339	/// Rest mass $m$ as timelike norm $\sqrt{-p_\mu p^\mu}$ in spacelike sign convention.
1340	#[must_use]
1341	#[inline]
1342	pub fn mass(&self) -> T {
1343		self.momentum.timelike_norm()
1344	}
1345
1346	/// Velocity $u^\mu$ as momentum $p^\mu$ divided by rest `mass()` $m$.
1347	#[must_use]
1348	#[inline]
1349	pub fn velocity(&self) -> OVector<T, D> {
1350		self.momentum.clone() / self.mass()
1351	}
1352
1353	/// Energy $E$ as [`Lorentzian::temporal`] component.
1354	#[must_use]
1355	#[inline]
1356	pub fn energy(&self) -> &T {
1357		self.momentum.temporal()
1358	}
1359
1360	/// Momentum $\vec p$ as [`Lorentzian::spatial`] components.
1361	#[must_use]
1362	#[inline]
1363	pub fn momentum(&self) -> VectorView<T, DimNameDiff<D, U1>, U1, D>
1364	where
1365		DefaultAllocator: Allocator<D, U1>,
1366		<DefaultAllocator as Allocator<D, U1>>::Buffer<T>:
1367			Storage<T, D, U1, RStride = U1, CStride = D>,
1368	{
1369		self.momentum.spatial()
1370	}
1371}
1372
1373impl<T, D> From<OVector<T, D>> for OMomentum<T, D>
1374where
1375	T: RealField,
1376	D: DimNameSub<U1>,
1377	DefaultAllocator: Allocator<D>,
1378{
1379	#[inline]
1380	fn from(momentum: OVector<T, D>) -> Self {
1381		Self { momentum }
1382	}
1383}
1384
1385impl<T, D> From<OMomentum<T, D>> for OVector<T, D>
1386where
1387	T: RealField,
1388	D: DimNameSub<U1>,
1389	DefaultAllocator: Allocator<D>,
1390{
1391	#[inline]
1392	fn from(momentum: OMomentum<T, D>) -> Self {
1393		momentum.momentum
1394	}
1395}
1396
1397impl<T, D> Neg for OMomentum<T, D>
1398where
1399	T: RealField,
1400	D: DimNameSub<U1>,
1401	DefaultAllocator: Allocator<D>,
1402{
1403	type Output = Self;
1404
1405	#[inline]
1406	fn neg(mut self) -> Self::Output {
1407		self.momentum = -self.momentum;
1408		self
1409	}
1410}
1411
1412impl<T, D> Add<Self> for OMomentum<T, D>
1413where
1414	T: RealField,
1415	D: DimNameSub<U1>,
1416	DefaultAllocator: Allocator<D>,
1417{
1418	type Output = Self;
1419
1420	#[inline]
1421	fn add(self, rhs: Self) -> Self::Output {
1422		Self {
1423			momentum: self.momentum + rhs.momentum,
1424		}
1425	}
1426}
1427
1428impl<T, D> Sub<Self> for OMomentum<T, D>
1429where
1430	T: RealField,
1431	D: DimNameSub<U1>,
1432	DefaultAllocator: Allocator<D>,
1433{
1434	type Output = Self;
1435
1436	#[inline]
1437	fn sub(self, rhs: Self) -> Self::Output {
1438		Self {
1439			momentum: self.momentum - rhs.momentum,
1440		}
1441	}
1442}
1443
1444impl<T, D> Copy for OMomentum<T, D>
1445where
1446	T: RealField + Copy,
1447	D: DimNameSub<U1>,
1448	DefaultAllocator: Allocator<D>,
1449	Owned<T, D>: Copy,
1450{
1451}
1452
1453/// Momentum in $2$-dimensional Lorentzian space $\R^{-,+} = \R^{1,1}$.
1454pub type Momentum2<T> = OMomentum<T, U2>;
1455/// Momentum in $3$-dimensional Lorentzian space $\R^{-,+} = \R^{1,2}$.
1456pub type Momentum3<T> = OMomentum<T, U3>;
1457/// Momentum in $4$-dimensional Lorentzian space $\R^{-,+} = \R^{1,3}$.
1458pub type Momentum4<T> = OMomentum<T, U4>;
1459/// Momentum in $5$-dimensional Lorentzian space $\R^{-,+} = \R^{1,4}$.
1460pub type Momentum5<T> = OMomentum<T, U5>;
1461/// Momentum in $6$-dimensional Lorentzian space $\R^{-,+} = \R^{1,5}$.
1462pub type Momentum6<T> = OMomentum<T, U6>;