Skip to content
Advanced

Levels per length, time and frequency

A level by itself is a number of Np or dB. Most of the time it comes attached to something it grows or shrinks over: a length of cable, a second of ringing, a hertz of ultrasound. Each of those is an ordinary Per<L, X>, and the same four rules answer every case:

Rate Where Type Over
dB/100 m, dB/km, Np/m a cable, a fibre, a sound path Per<Loss<L>, X>, X a length Fixed::over a length is the total loss
Np/s, dB/s a resonator, an AGC ramp Per<Loss<Neper>, Second> DampingCoefficient and its derived \(\zeta\), \(Q\), time constant
dB/(cm·MHz) medical ultrasound Per<Loss<Decibel>, CentimetreMegahertz> ×f first, then over a depth
AU, \(\tau\) (Beer-Lambert) a cuvette, a filter Absorbance, OpticalDepth both exact scales of the same loss base, no form in between

None of these mixes with the others by conversion: a cable's \(\alpha\) is not a wavenumber, a resonator's \(\delta\) is not a hertz, and a transmittance's \(\tau\) is not a level of amplitude. The rest of this page is why, with the numbers worked through the crate's own types rather than a borrowed f64.

Loss or gain

Decibels covers the sign in full; the short version is the one this page leans on everywhere below. A level is signed as a gain (Neper, Decibel: a decay is negative) or as a loss (Loss<Neper>, Loss<Decibel>: a decay is positive, as every datasheet and ISO 80000-3 write it). The two don't convert into each other; as_gain and as_loss negate:

use exact_units::{Fixed, prefix::Centi, units::{Decibel, Loss}};

// a 40 dB pad written as a gain, and the same number as the loss on its box
let pad_gain = Fixed::<Decibel, Centi, i32>::new(-4000);
let pad_loss: Fixed<Loss<Decibel>, Centi, i32> = pad_gain.as_loss();
assert_eq!(pad_loss.get(), 4000);
assert_eq!(pad_loss.as_gain(), pad_gain);

Attenuation per length

Metres100, Feet100 and Kilometre are lengths of their own so that a rate prints as its datasheet does, and Fixed::over turns a rate into the loss across a run of cable. It multiplies in f64 and rounds the product once by the Rounding you pass, as times_into does. A single-mode fibre's spec sheet, 0.18 dB/km at 1550 nm, over an 80 km span:

use exact_units::{Fixed, Rounding::NearestEven, prefix::{Centi, One}, units::{Decibel, Kilometre, Loss, Per}};

let alpha = Fixed::<Per<Loss<Decibel>, Kilometre>, Centi, i32>::new(18); // 0.18 dB/km
let span = Fixed::<Kilometre, One, i32>::new(80);
let budget: Fixed<Loss<Decibel>, Centi, i32> = alpha.over(span, NearestEven).unwrap();
assert_eq!(budget.get(), 1440); // 14.4 dB over the span

For the full story in one figure — a wave dying along an RG-58 coax, its datasheet's dB/100 ft and the SI dB/100 m agreeing to the last digit — see Decibels.

Damping per time

Damping carries the ISO 80000-3 table in full: \(\delta\), \(\Lambda\), \(\zeta\), \(Q\) and the time constant, with dB/s ramps and every refusal. Here is the short chain that gets used the most, \(\delta\) and \(\omega_0\) to \(\zeta\) and \(Q\), for a loop that rings at 1000 rad/s and settles by 50 Np/s:

use exact_units::{Fixed, Rounding::NearestEven, prefix::{Milli, One}};
use exact_units::units::{AngularVelocity, DampingCoefficient, DampingRatio, QualityFactor};

let delta = Fixed::<DampingCoefficient, One, i32>::new(50);
let omega0 = Fixed::<AngularVelocity, One, i32>::new(1000);
let zeta: Fixed<DampingRatio, Milli, i32> = delta.damping_ratio(omega0, NearestEven).unwrap();
assert_eq!(zeta.get(), 50); // ζ = 0.05
let q: Fixed<QualityFactor, One, i32> = zeta.quality_factor(NearestEven).unwrap();
assert_eq!(q.get(), 10);

\(\delta\) is Np/s, \(\omega_0\) is rad/s, and damping_ratio only compiles for that pair: a damping coefficient divided by a frequency in Hz would lose a factor of \(2\pi\), so the types ask for the angular one.

Frequency-scaled attenuation: ultrasound

Medical ultrasound attenuates more at higher frequencies: \(\alpha(f) = \alpha_0 \cdot f\), with \(\alpha_0\) in dB per CentimetreMegahertz (soft tissue \(\approx 0.5\)). α₀ · f is a product (times_into) whose unit — cm·MHz divided by hertz — is already a length by the product table's own dimension rule, so one convert lands it in dB/m, and over a depth gives the one-way loss:

use exact_units::{Fixed, Rounding::NearestEven, prefix::{Centi, Deci, Mega, One}};
use exact_units::units::{CentimetreMegahertz, Decibel, Hertz, Loss, Meter, Per};

// soft tissue's 0.5 dB/(cm·MHz) at 5 MHz is 2.5 dB/cm
let alpha0 = Fixed::<Per<Loss<Decibel>, CentimetreMegahertz>, Deci, i32>::new(5);
let f = Fixed::<Hertz, Mega, i32>::new(5);
let product = alpha0.times_into::<Hertz, Mega, _, Deci, i32>(f, NearestEven).unwrap();
let alpha: Fixed<Per<Loss<Decibel>, Meter>, One, i32> = product.convert().unwrap();
assert_eq!(alpha.get(), 250); // 2.5 dB/cm = 250 dB/m

// through 4 cm one way, and the round trip an echo makes of it
let path = Fixed::<Meter, Centi, i32>::new(4);
let one_way: Fixed<Loss<Decibel>, Deci, i32> = alpha.over(path, NearestEven).unwrap();
assert_eq!(one_way.get(), 100); // 10.0 dB
assert_eq!((one_way + one_way).get(), 200); // 20.0 dB there and back
0246 cm0102030 dBdepthone-way loss2 MHz5 MHz8 MHz4 cm, 10 dB
One-way loss against depth at 2, 5 and 8 MHz, soft tissue's 0.5 dB/(cm·MHz): the line through 5 MHz and 4 cm is the worked example above, 10 dB one way. A higher frequency loses more per centimetre, which is the clinical trade-off between penetration depth and resolution. Computed by tools/book_plots with the crate's own times_into, convert and over.

A power law \(\alpha_0 \cdot f^y\) with \(y \ne 1\) (some tissues, some frequency ranges) is worked out in floats by the caller; only the linear case is a type here. And the frequency has to be a frequency, not an angular velocity — cm·MHz divided by rad/s isn't a length:

use exact_units::{Fixed, Rounding, prefix::One, units::{AngularVelocity, CentimetreMegahertz, Decibel, Loss, Meter, Per}};

let alpha0 = Fixed::<Per<Loss<Decibel>, CentimetreMegahertz>, One, i32>::new(5);
let omega = Fixed::<AngularVelocity, One, i32>::new(5);
let product = alpha0.times_into::<AngularVelocity, One, _, One, i32>(omega, Rounding::NearestEven).unwrap();
let _: Option<Fixed<Per<Loss<Decibel>, Meter>, One, i32>> = product.convert();

Beer-Lambert: absorbance, optical depth and \(\mu = 2\alpha\)

Decibels has the full cuvette example, \(\varepsilon \cdot l \cdot c\) to an Absorbance and on to a transmittance. The one identity worth isolating here is the factor of 2 between the natural attenuation coefficient of a beam's power, \(\mu\), and the \(\alpha\) of a loss per length in nepers: \(\mu\) is defined without the level's own \(\tfrac12\), so the conversion doubles it, exactly, every time:

use exact_units::{Fixed, Rounding, prefix::{Micro, One}};
use exact_units::units::{Decibel, LinearAttenuation, Loss, Meter, Neper, Per};

// a turbid medium's 10 dB/m of power loss; dB to Np is ln 10/20, no fraction, so the float is a named rounding
let per_metre = Fixed::<Per<Loss<Decibel>, Meter>, One, f64>::bounded(10.0);
let alpha = per_metre.convert_rounded::<Per<Loss<Neper>, Meter>, Micro, f64>(Rounding::NearestEven).unwrap();
let mu = per_metre.convert_rounded::<LinearAttenuation, Micro, f64>(Rounding::NearestEven).unwrap();
assert!((mu.get() - 2.0 * alpha.get()).abs() < 1e-9);
\[ \mu = 2\alpha \qquad \tau = \ln 10 \cdot A \]

Absorbance (\(A\), decadic) and optical depth (\(\tau\), natural) are the same Beer-Lambert loss on two exact scales of the loss base, so the factor of \(\ln 10\) between them is a convert_rounded, never a float someone has to remember.

Why Np/s, rad/s and Hz are kept apart

All three are s⁻¹ in the SI. Here they are three different dimensions, for the same reason the radian is its own dimension and not folded into "dimensionless": a hertz, an angular velocity and a damping coefficient answer different questions, and conflating them is the easiest way to lose a factor of \(2\pi\) or a sign. Dim::strip is the explicit bridge: it drops one named base, the radian for the first pair, the neper of a loss for levels:

use exact_units::{MarkerId, Unit, units::{AngularVelocity, DampingCoefficient, Hertz}};

assert_eq!(AngularVelocity::DIM.strip(MarkerId::RADIAN), Hertz::DIM);
assert_eq!(DampingCoefficient::DIM.strip(MarkerId::NEPER_LOSS), Hertz::DIM);
assert_ne!(AngularVelocity::DIM, Hertz::DIM); // rad/s stays rad/s until it's stripped

Neither bridge is ever applied by a conversion; both are named steps for an export to a system that doesn't carry the distinction (UCUM, Pint, uom). Inside the crate, a damping coefficient refuses to become a hertz:

use exact_units::{Fixed, prefix::One, units::{Hertz, Neper, Per, Second}};

let _ = Fixed::<Per<Neper, Second>, One, f32>::bounded(3.0).convert::<Hertz, One, f32>();

and neither does a cable's attenuation become a wavenumber, however much it looks like m⁻¹ in SI:

use exact_units::{Fixed, prefix::One, units::{Meter, Neper, Per, Wavenumber}};

let _ = Fixed::<Per<Neper, Meter>, One, f32>::bounded(0.02).convert::<Wavenumber, One, f32>();

dBm/Hz is not a rate

dBm/Hz and dBc/Hz are levels of a spectral density (\(10 \lg\) of a power per hertz against 1 mW/Hz), not a level per frequency: Per<Decibel, Hertz> is the wrong type for them, as the Damping page's warning says. They belong with the referenced levels (dBm, dBFS), not with the rates on this page.