comnum_core/units.rs
1//! Unit newtypes for the quantities that are routinely confused.
2//!
3//! A decibel value is a [`Db`], never a bare `f64`; an `E_{b}/N_{0}` is an [`EbN0`] and
4//! an `E_{s}/N_{0}` an [`EsN0`], so that passing one where the other is expected is a
5//! compile error. Conversions are explicit methods, never `From` impls that hide a
6//! formula.
7//!
8//! The decibel conversions of plain numbers ([`db2lin`], [`lin2db`], [`db2amp`],
9//! [`amp2db`], [`dbm2w`], [`w2dbm`], [`db2np`], [`np2db`]) are defined here, once: the
10//! methods of the newtypes call them, and `comnum_math` re-exports them (`comnum_math::db2lin`
11//! is this function), so that a value in dB converts to the same bits whichever way it
12//! is written.
13//!
14//! # Model
15//!
16//! With `k` coded bits per symbol and a code rate `R`,
17//!
18//! $$
19//! \left.\frac{E_{s}}{N_{0}}\right|_{\mathrm{dB}} = \left.\frac{E_{b}}{N_{0}}\right|_{\mathrm{dB}} + 10 \log_{10}(k R)
20//! $$
21//!
22//! # Examples
23//! ```
24//! use comnum_core::units::{Db, EbN0};
25//! let es = EbN0(Db(4.0)).to_es_n0(2.0, 1.0); // QPSK, uncoded
26//! assert!((es.0.0 - 7.0103).abs() < 1e-4);
27//! assert!((Db(3.0).to_linear() - 1.9953).abs() < 1e-4);
28//! assert!((Db::from_linear(10.0).0 - 10.0).abs() < 1e-12);
29//! // the newtype and the function are one conversion
30//! assert_eq!(Db(3.0).to_linear(), comnum_core::units::db2lin(3.0));
31//! // 2.6 information bits per symbol (a shaped or coded constellation)
32//! let es = EbN0(Db(4.0)).to_es_n0(2.6, 1.0);
33//! assert!((es.0.0 - (4.0 + comnum_core::units::lin2db(2.6))).abs() < 1e-12);
34//! ```
35
36/// Power ratio in dB to linear: `10^{x/10}`.
37#[inline]
38pub fn db2lin(x_db: f64) -> f64 {
39 10f64.powf(x_db / 10.0)
40}
41
42/// Linear power ratio to dB: `10 log10(x)`.
43#[inline]
44pub fn lin2db(x: f64) -> f64 {
45 10.0 * x.log10()
46}
47
48/// Amplitude ratio in dB to linear: `10^{x/20}`.
49#[inline]
50pub fn db2amp(x_db: f64) -> f64 {
51 10f64.powf(x_db / 20.0)
52}
53
54/// Linear amplitude ratio to dB: `20 log10(x)`.
55#[inline]
56pub fn amp2db(x: f64) -> f64 {
57 20.0 * x.log10()
58}
59
60/// Attenuation in dB per unit length to nepers (power): `α_dB / (10 log10 e)`.
61#[inline]
62pub fn db2np(x_db: f64) -> f64 {
63 x_db / (10.0 * std::f64::consts::LOG10_E)
64}
65
66/// Attenuation in nepers (power) per unit length to dB: `10 log10(e) · α`.
67#[inline]
68pub fn np2db(x_np: f64) -> f64 {
69 10.0 * std::f64::consts::LOG10_E * x_np
70}
71
72/// Power in dBm to watts: `10^{P/10} · 1 mW`.
73#[inline]
74pub fn dbm2w(p_dbm: f64) -> f64 {
75 1e-3 * db2lin(p_dbm)
76}
77
78/// Power in watts to dBm.
79#[inline]
80pub fn w2dbm(p_w: f64) -> f64 {
81 lin2db(p_w / 1e-3)
82}
83
84/// A power ratio in decibels.
85#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
86pub struct Db(pub f64);
87
88impl Db {
89 /// The linear power ratio `10^{x/10}` ([`db2lin`]).
90 pub fn to_linear(self) -> f64 {
91 db2lin(self.0)
92 }
93
94 /// The decibel value `10 log_{10}(x)` of a linear power ratio ([`lin2db`]).
95 pub fn from_linear(x: f64) -> Db {
96 Db(lin2db(x))
97 }
98}
99
100/// Energy per information bit over the noise spectral density, `E_{b}/N_{0}`.
101#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
102pub struct EbN0(pub Db);
103
104/// Energy per symbol over the noise spectral density, `E_{s}/N_{0}`.
105#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
106pub struct EsN0(pub Db);
107
108impl EbN0 {
109 /// Converts to `E_{s}/N_{0}` for `bits_per_symbol` coded bits per symbol at code
110 /// rate `rate`: $E_{s}/N_{0} = E_{b}/N_{0} + 10 \log_{10}(k R)$ dB. The number of bits
111 /// is real: the entropy of a shaped constellation, for instance.
112 pub fn to_es_n0(self, bits_per_symbol: f64, rate: f64) -> EsN0 {
113 EsN0(Db(self.0.0 + lin2db(bits_per_symbol * rate)))
114 }
115}
116
117impl EsN0 {
118 /// Converts to `E_{b}/N_{0}` for `bits_per_symbol` coded bits per symbol at code
119 /// rate `rate` (the inverse of [`EbN0::to_es_n0`]).
120 pub fn to_eb_n0(self, bits_per_symbol: f64, rate: f64) -> EbN0 {
121 EbN0(Db(self.0.0 - lin2db(bits_per_symbol * rate)))
122 }
123}
124
125/// A frequency or a rate (sample rate, symbol rate), in hertz.
126#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
127pub struct Hertz(pub f64);
128
129/// A duration, in seconds.
130#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
131pub struct Seconds(pub f64);
132
133impl Hertz {
134 /// The period `1/f`.
135 pub fn period(self) -> Seconds {
136 Seconds(1.0 / self.0)
137 }
138}
139
140impl Seconds {
141 /// The frequency `1/T`.
142 pub fn frequency(self) -> Hertz {
143 Hertz(1.0 / self.0)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn conversions_round_trip() {
153 let eb = EbN0(Db(6.0));
154 let back = eb.to_es_n0(4.0, 0.75).to_eb_n0(4.0, 0.75);
155 assert!((back.0.0 - 6.0).abs() < 1e-12);
156 assert!((Hertz(32e9).period().frequency().0 - 32e9).abs() < 1e-3);
157 assert!((Db::from_linear(Db(-7.5).to_linear()).0 + 7.5).abs() < 1e-12);
158 }
159}