Skip to main content

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}