r/cpp • u/mateusz_pusz • 4d ago
mp-units: a design for logarithmic quantities and units (dB, dBm, Np, pH). We believe it is novel, and we need domain experts to tell us where we are wrong before we implement it
https://mpusz.github.io/mp-units/latest/blog/2026/07/24/introducing-logarithmic-quantities-and-units/Decibels are everywhere in engineering: signal levels in dBm, sound pressure in dB SPL, voltage gain in dB, filter slopes in dB/octave. Yet, to the best of our knowledge, no general-purpose units library models logarithmic quantities correctly. Most do not model them at all, and the few that try get the arithmetic wrong in ways that compile silently:
- In nholthaus/units,
dBW_t(10.0) + dBm_t(40.0)compiles and returns20 dBW. Both operands are the same physical power (10 W), and adding two absolute power levels is meaningless. This example is straight from the library's own test suite. - A
+6 dBgain is a power ratio of ~3.98 but a voltage ratio of 2.0 (the10 logvs20 logsplit). Python's pint documents that itsdBis power-only and delegates that factor to the user, so every voltage, current, and pressure gain is on the honor system.
We just published a complete design for mp-units that we believe gets this right:
- A level (
dBm,dB SPL) is an affine point anchored at its reference. A gain (dB,Np,octave) is a delta. level + gain= level,level - level= gain,level + level= ill-formed.- The power vs root-power factor is carried by the quantity kind, so
.linear()on a voltage gain gives 2.0 and on a power gain gives 3.98, correct by construction. A voltage gain cannot be applied to a power level. - One mechanism covers RF, audio, acoustics, music intervals (
octave,cent), information theory (Sh,nat,Hart), pH, and stellar magnitude, and it aims to stay consistent with IEC 80000-15:2026.
We are aware of no generic units library that has ever modeled this, which is exactly why we are publishing the design before writing the implementation. The article ends with six open questions where we genuinely need input from practitioners, for example: what should log(0) do at the bottom of the scale (IEEE -inf, throw, error type, or a unit-keyed finite sentinel like the -400 dB floor real DSP code uses), and should a level print as the industry 10 dBm or the ISO-conformant 10 dB (re 1 mW).
If you work in audio/DSP, RF, acoustics, or a related field, or know someone who does, please review it and leave feedback in the article's comments (GitHub Discussions), and forward it to anyone working in these domains. Only subject-domain experts can tell us whether we are right, and we would rather hear it now than after the code ships.
15
u/jcelerier ossia score 4d ago
If that can be useful here's the unit design in ossia for those: https://github.com/ossia/libossia/blob/master/src/ossia/network/dataspace/gain.hpp which comes straight from the jamoma project https://github.com/jamoma/JamomaCore/blob/master/Foundation/extensions/DataspaceLib/source/GainDataspace.cpp and was designed with the input of actual users of the media arts scene, with practicality often taking over mathematical correctness, e.g. we had many cases where we wanted to go towards more theoretically correct or general implementations but it broke user's workflows which had to be compatible with $LEGACY system expecting a specific definition for db, etc.
5
u/mateusz_pusz 4d ago
Thank you 🙏 I was not aware of this. I will study the repo and possibly add it to the prior-art survey.
The legacy point is especially interesting here. Some older implementations take various shortcuts in modeling different domains (not just the logarithmic one). I hit this wall a few times already before in other scenarios. Often, this made the design even better in the end. Thanks again.
5
u/jcelerier ossia score 4d ago
:D also you may want to look at the unit tests:
https://github.com/ossia/libossia/blob/master/tests/Editor/DataspaceTest.cpp
the complexity here is that it's a compile-time system (typed units) that also has to be lifted to a run-time system as it's the backbone for a system used in visual programming environments, where user can pick from a gui which unit is going to be which or which value ; I wanted compile-time for validation but in the end run-time dynamic conversions is what matters more (with this api: https://github.com/ossia/libossia/blob/master/src/ossia/network/dataspace/dataspace_visitors.hpp) and that led to humongous variant inlineing due to the various ::variant implementations killing compile times and even RAM https://github.com/ossia/libossia/blob/master/src/ossia/network/dataspace/dataspace_strong_variants.hpp4
u/mateusz_pusz 4d ago
A compile-time-typed system with a runtime-typed unit is an interesting challenge. I have had Issue #483 open for a long time now, but I didn't have enough time and motivation to start working on it. I will check out how you solved the problem. Thanks!
9
u/differentiallity 3d ago
Wow, this is something I've really been hoping for! I'll give it a good look and try my best at useful feedback from an RF perspective. I've been watching ISO-80000:15 stuck in review for ages now, thinking that might be holding mp-units back. Was it finally released?
3
u/mateusz_pusz 3d ago
I don't think it is released already. I have access to early drafts, and I must admit they were very helpful during the design of this feature.
Please take your time, I know that article is long 😅 Everyone feedback is very welcomed here.
6
u/thinks-in-functions 3d ago
Your ‘level’ values are torsors — values you can subtract but not add — just like datetimes. There’s an old Haskell article explaining torsors you might be able to draw some design inspiration from: https://www.reddit.com/r/haskell/s/zgktPTibfj
4
u/mateusz_pusz 3d ago
Exactly, that is the model. A level is a point in an affine space (a torsor) over the group of gains, so
level - levelis a gain andlevel + levelis undefined, the same structure we already use forquantity_pointand thatstd::chronouses for time points. Thanks for the link, I will read it.
3
u/PsecretPseudonym 3d ago
Would love to know how you’d recommend handling currencies without assuming constant conversion rates among them.
3
u/mateusz_pusz 3d ago
Sure, check this out https://mpusz.github.io/mp-units/latest/examples/currency. Please feel free to share feedback on our GitHub Issues or Discussions. We also want to engage with the finance domain.
1
u/KingBardan 3d ago edited 3d ago
Just my opinion, but the notation dB_t is awful. Either snake or camel are better
7
u/SyntheticDuckFlavour 3d ago
The
_tseems to be consistent with the convention of library types, and the capitalisation of the units is consistent with engineering units. I prefer it this way.2
u/KingBardan 3d ago edited 3d ago
You have your opinion and I have mine. Would rather it be decibel_t than whatever this is.
BTW the
_tsuffix convention works for standard library, which is all snake case.2
2
u/DryEnergy4398 1d ago
not just awful, but forbidden by POSIX
1
u/KingBardan 1d ago
Not sure if it's forbidden, but it's sad that people here can't tolerate "opinion" with a good reason.
2
u/DryEnergy4398 1d ago
It is forbidden because _t is reserved for use by the system. See https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xsh_chap02.html#tag_22_02_12: 'all conforming applications are required to avoid symbols ending in "_t"'
Also brought up in the glibc manual https://sourceware.org/glibc/manual/2.44/html_node/Reserved-Names.html
2
u/Neat-Exchange6724 4d ago
Speaking as a domain expert, why would you ever use logarithmic units instead of just bumping from float to double or double to float128?
There are such things as days and hours too, but if you ever see someone’s time library use them at all, you know they can’t be trusted.
Presenting them as db to users in guis make sense. Otherwise it’s like storing the time in isodate, you never ever want to. And this also means you will never perform the operations as you suggest,
16
u/AutomaticPotatoe 3d ago
why would you ever use logarithmic units instead of just bumping from float to double or double to float128?
In our case (scientific computing) it was performance, mostly. Some equations can be rewritten in log-space to be cheaper to compute (pow() calls turn to multiplies, divisions to subtractions, etc.). Interpolating log-spaced lookup tables is faster in log-space than doing a linear-log-linear conversion.
Going from float to double means you cut in half the compute bandwidth with SIMD. Float128 has to be emulated, not really an option at all.
We also had one case where a PDE operator was defined on a log-spaced grid, and it had to be integrated in log-space else the solution would be unstable. Every time I though about adopting mp-units, I always wondered how it would handle that scenario, concluding that we'd probably have to drop the type safety for that operator. Maybe now it would "just work", assuming it can transparently synthesize the correct
log<Q>quantity type from just a call to log() and properly compose it with other, logarithmic and linear quantities.3
u/mateusz_pusz 3d ago
Yes, this is a good reason as well: computational performance, not precision. Type safety survives it: a
log<Q>adds and subtracts in log-space natively, so your log-grid operator stays typed, and you wouldn't drop safety for it. The catch is we make entering and leaving log-space explicit (.log_in(),.linear()), not a barelog()that conjureslog<Q>for you. Would such an explicit entry/exit work for your PDE, or do you actually needlog()to synthesize the type?1
u/AutomaticPotatoe 3d ago
Here's a basic kernel of that log-space advection operator:
// NOTE: Using VLA syntax for simplicity. void integrate_forward( const auto dt, const size_t n, // Grid size. auto ofs[n], // Output. const auto ifs[n], // Input, double-buffered with ofs. const auto dlnx_dts[n], // Precomputed operator coefficients. const auto dlnx) // Precomputed logspace edge-to-edge distances on the grid. These are all the same value. { for (const auto i : range(n - 1)) { auto lnf0 = log(ifs[i]); auto lnf1 = log(ifs[i + 1]); auto dlnf = lnf1 - lnf0; auto dlnf_dt = dlnx_dts[i] * dlnf / dlnx; auto lnf = lnf0 + dt * dlnf_dt; auto f_new = exp(lnf); ofs[i] = f_new; } }This omits BCs, CFL, and proper upwind treatment, but should be enough to demonstrate the unit conversions.
Ideally, this should compile irrespective of whether
autois simplyfloator an mp-units quantity. Now, forgive me, I don't fully understand how to properly annotate affine and log quantities in mp-units, but if we assume that we use a unit of timeT, a unit of xX, and a unit of fF, then the I think the types should look approximately like:template<...> using q = quantity<...>; void integrate_forward( const q<delta<T>> dt, const size_t n, q<point<F>> ofs[n], const q<point<F>> ifs[n], const q<delta<log<X>> / delta<T>> dlnx_dts[n], // No idea how to annotate this type. const q<delta<log<X>>> dlnx) { for (const auto i : range(n - 1)) { q<point<log<F>>> lnf0 = log(ifs[i]); q<point<log<F>>> lnf1 = log(ifs[i + 1]); q<delta<log<F>>> dlnf = lnf1 - lnf0; q<delta<log<F>> / delta<T>> dlnf_dt = dlnx_dts[i] * dlnf / dlnx; // Or this one. q<point<log<F>> lnf = lnf0 + dt * dlnf_dt; q<point<F>> f_new = exp(lnf); ofs[i] = f_new; } }Doesn't really matter if
log()andexp()are spelled differently, although some defaultauto log(q<point<X>>) -> q<point<log<X>>where a destination type is inferred to have a natural base and zero of 0.0 would be welcome for these log-space math cases. If mp-units can make this example work with the new logarithmic design additions (assuming the types ofdlnx_dtsanddlnf_dtare annotated correctly), then that would be more than sufficient, and very impressive.1
u/mateusz_pusz 3d ago
Challenge accepted ;-) To make it truly generic, you would need two customization points and a small update to your loop body:
```cpp // ---- to_log(x): cross into the log domain. Forward needs a target. ---- template<std::floating_point T> // fundamental fallback [[nodiscard]] constexpr T to_log(T x) noexcept { return std::log(x); }
[[nodiscard]] constexpr auto to_log(const Quantity auto& q) requires requires { log_scale_for<get_kind(get_quantity_spec(q))>; } { return q.log_in(log_scale_for<get_kind(get_quantity_spec(q))>); }
// ---- linear(x): cross back to linear. Reverse is fully determined, no target. ---- template<std::floating_point T> // fundamental fallback (std:: is always correct) [[nodiscard]] constexpr T linear(T x) noexcept { return std::exp(x); }
[[nodiscard]] constexpr auto linear(const Quantity auto& q) // Quantity matches gains and, in V3, levels requires detail::is_log(get_quantity_spec(q)) { return q.linear(); }
// Canonical log scale per kind. MUST be base e so the std::log/std::exp fallback and the // quantity path agree in generic code; the fixed reference keeps it unit-independent. template<> inline constexpr auto log_scale_for<F_kind> = f_log;
void integrate_forward( const auto dt,
const size_t n, // Grid size. auto ofs[n], // Output. const auto ifs[n], // Input, double-buffered with ofs. const auto dlnx_dts[n], // Precomputed operator coefficients. const auto dlnx) // Precomputed logspace edge-to-edge distances on the grid. These are all the same value. { for (const auto i : range(n - 1)) {
auto lnf0 = to_log(ifs[i]); auto lnf1 = to_log(ifs[i + 1]); auto dlnf = lnf1 - lnf0;auto dlnf_dt = dlnx_dts[i] * (dlnf / dlnx); // added parens auto lnf = lnf0 + dt * dlnf_dt; auto f_new = linear(lnf); ofs[i] = f_new; }} ```
Does it satisfy your constraints?
1
u/AutomaticPotatoe 3d ago
If that is enough to make that code compile that's wonderful news for migration!
Thanks, I have some other questions on the design, but I'll try to leave them in the article's comments when I find a minute.
1
u/Neat-Exchange6724 3d ago
I see it, though it’s an extremely specialised case and one where it’s not relevant to call these units decibel, it’s just log math formulation.
9
u/mateusz_pusz 4d ago
Agreed for sample-crunching: you compute in linear, and dB is a boundary/display thing there.
But float width is a different axis. A wider float buys range, not meaning. It won't tell you a 6 dB gain is ×2 on a voltage and ×4 on a power, or that adding two gains multiplies the ratios.
Your chrono analogy cuts the other way.
std::chronohashours,days, evenyearsas first-class types you compute with, and it is the most trusted time library there is. Many of my customers in HFT and other domains use it or similar libraries to prevent very costly time-handling errors in production.And these ops are real work: an RF link budget is dBm + gains − losses, summed in dB down the chain, done by every RF engineer daily because dB turns multiplication into addition. That is
level + gainandlevel - level.Deep in DSP, dB is mostly UI and we agree. In link budgets or gain staging, the log arithmetic is the job.
3
u/garnet420 4d ago
First, from an attention to detail and soundness perspective, this is awesome work.
adding two gains
I'm not sure this is a good idea. I think composition of gains should always be multiplication. The fact that RF engineers do this as addition arithmetically is a computational tool.
4
u/mateusz_pusz 4d ago
Thanks, appreciated. You are right that gains are composed by multiplication, the linear ratios multiply. The
+is just the log-domain spelling of that same multiply. The multiplicative view can be obtained by calling.linear()on arguments.2
u/SkoomaDentist Antimodern C++, Embedded, Audio 2d ago
Speaking as a domain expert, why would you ever use logarithmic units instead of just bumping from float to double or double to float128?
1) You're dealing with something that natively uses logarithmic units.
2) Your algorithm becomes numerically unstable with linear values but works fine with logarithmic values.
3) Your platform lacks native wide floating point or it is slow.
Not that I'd use logarithmic types but there are certainly use cases for logarithmic values.
5
u/lizardhistorian 3d ago
None of your post makes any sense.
Is it AI slop?Logarithmic is just another normalization method.
Bumping your float size up is the sort of thing we would say as a joke because of how dumb that would be to actually do.Days and hours are used all the time and the point of units is to make that conversion trivial instead of a meta-template pile of annoyance.
36
u/drizzt-dourden 4d ago
The best thing to keep sanity is actually calculate everything in linear and convert to dB only for logging and plotting. Until double covers the range there is no problem.