@@ -29,14 +29,6 @@ polymorphism.
2929This article describes the motivation in depth, the design we arrived at, and
3030the open questions we would love the community's help to answer.
3131
32- !!! note "Work in progress"
33-
34- This feature is shipping in the current development version of **mp-units**.
35- We are planning a further extension for **non-negative quantity annotations**
36- (quantities that live on a ratio scale and are always ≥ 0, such as _mass_,
37- _duration_, or _electric charge_). That extension will be covered in a
38- follow-up article once the implementation is complete.
39-
4032<!-- more -->
4133
4234## The Problem
@@ -230,7 +222,7 @@ parent's bounds:
230222// static_assert fires at compile time if relative bounds exceed parent bounds
231223template <>
232224inline constexpr auto mp_units::quantity_bounds<ac_setpoint> =
233- mp_units:: clamp_to_range{delta<deg_C>(-3), delta<deg_C>(+3)};
225+ clamp_to_range{delta<deg_C>(-3), delta<deg_C>(+3)};
234226 // ❌ compile error if this would violate the parent origin's physical bounds
235227```
236228
@@ -245,7 +237,7 @@ instantiated by enforcing the following, in order:
245237 by the cumulative offset) must nest strictly inside the parent's range.
246238
247239Both checks are **compile-time** `static_assert`s. They fire exactly once per
248- specialisation regardless of how many `quantity_point` variables are constructed.
240+ specialization regardless of how many `quantity_point` variables are constructed.
249241
250242### Full example: geodetic coordinate types
251243
@@ -264,7 +256,7 @@ inline constexpr struct equator final : absolute_point_origin<geo_latitude> {}
264256inline constexpr struct prime_meridian final : absolute_point_origin<geo_longitude> {} prime_meridian;
265257```
266258
267- Outside the anonymous namespace (so the specialisations have external linkage):
259+ Outside the anonymous namespace (so the specializations have external linkage):
268260
269261``` cpp
270262template <>
@@ -419,6 +411,47 @@ guarantees described above apply here too: you could use `constrained<float>` or
419411` constrained<int> ` as the representation without changing the
420412` quantity_bounds ` specialization.
421413
414+ ### Non-negative quantity annotations
415+
416+ [ Absolute quantities] ( introducing-absolute-quantities.md ) are quantities that
417+ live on a ratio scale — always measured from a natural zero — such as _ mass_ ,
418+ _ duration_ , or _ electric charge magnitude_ . Non-negativity is the canonical
419+ constraint for all of them, and ** mp-units** now implements it at the
420+ quantity-specification level.
421+
422+ The ` non_negative ` flag can be applied to any real-scalar base or named child
423+ quantity spec, and the library propagates the flag transitively: a quantity
424+ derived from two non-negative specs is itself non-negative.
425+
426+ ``` cpp
427+ static_assert (is_non_negative(isq::length)); // ✅ tagged in ISQ system definition
428+ static_assert (is_non_negative(isq::mass)); // ✅ tagged in ISQ system definition
429+ static_assert (is_non_negative(isq::speed)); // ✅ derived: length / duration
430+ static_assert (!is_non_negative(isq::velocity)); // ❌ vector character — excluded
431+ ```
432+
433+ !!! note
434+
435+ `kind_of<QS>` is **never** non-negative, even when `QS` itself is tagged `non_negative`,
436+ because `kind_of<QS>` represents the entire quantity tree including vector quantities
437+ and signed coordinates. This matters when using CTAD with bare SI units:
438+
439+ ```cpp
440+ quantity_point generic{5.0 * m}; // origin uses kind_of<isq::length> — NOT auto-bounded
441+ quantity_point dist{distance_traveled(5.0 * m)}; // uses isq::distance — auto-bounded
442+ ```
443+
444+ When a ` quantity_point ` uses a ` natural_point_origin ` whose quantity spec is
445+ non-negative, the library ** automatically attaches ` check_non_negative ` ** as the
446+ bounds policy — no explicit ` quantity_bounds ` specialization is needed. The
447+ default can always be overridden:
448+
449+ ``` cpp
450+ // Override the auto-applied check_non_negative with a clamping policy instead:
451+ template <>
452+ inline constexpr auto mp_units::quantity_bounds<natural_point_origin<isq::length>> = clamp_non_negative{};
453+ ```
454+
422455---
423456
424457## Design Trade-offs and Open Questions
@@ -432,18 +465,12 @@ misleading (there is no "smallest" longitude on a wrapped circle; they're all
432465equivalent modulo 360°). The current implementation does return ` min ` and ` max `
433466for all policy types that expose these members.
434467
435- ### Should ` quantity_bounds ` be supported on absolute quantities?
436-
437- [ Absolute quantities] ( introducing-absolute-quantities.md ) are quantities that
438- live on a ratio scale — always measured from a natural zero — such as _ mass_ ,
439- _ duration_ , or _ electric current magnitude_ . Non-negativity is the canonical
440- constraint for them and will be addressed by a dedicated language-level
441- annotation in a future ** mp-units** release.
468+ ### Should ` quantity_bounds ` be applied to application-level absolute quantity ranges?
442469
443- Beyond non-negativity, do you see real use cases for attaching
444- _ application-specific_ bounds to absolute quantities directly — for example,
445- clamping a sensor's mass reading to its physical measurement range, or bounding
446- a duration to a maximum scheduling window?
470+ Beyond the automatic non-negativity enforcement described above , do you see real
471+ use cases for attaching _ application-specific_ range bounds to absolute quantities
472+ directly — for example, clamping a sensor's _ mass _ reading to its physical
473+ measurement range, or bounding a _ duration _ to a maximum scheduling window?
447474
448475---
449476
@@ -460,7 +487,7 @@ domain, we would love to hear from you:
460487- Did the design make your use case straightforward to express?
461488- Were there cases where you reached for bounds but the current design would not
462489 cover them?
463- - Do you have strong opinions on the open questions above?
490+ - Do you have a view on either of the two open questions above?
464491
465492Please join the conversation in the
466493[ GitHub Discussions] ( https://github.com/mpusz/mp-units/discussions ) or open a
@@ -470,14 +497,15 @@ Please join the conversation in the
470497
471498## Summary
472499
473- | | Before | After |
474- | -------------------------------------------------| -----------------------| -------------------------------------|
475- | Latitude type enforces pole constraint | ❌ user responsibility | ✅ compile-time, zero-overhead |
476- | Longitude wraps cyclically | ❌ manual modulo | ✅ ` wrap_to_range ` on origin |
477- | Sensor range clamped at API boundary | ❌ runtime if-else | ✅ policy on origin |
478- | Relative origin inherits parent bounds | ❌ impossible | ✅ automatic, static-checked nesting |
479- | ` min() ` /` max() ` /` numeric_limits ` reflect bounds | ❌ reports type limits | ✅ reports domain limits |
480- | Half-line (non-negative) bounds | ❌ impossible | ✅ custom policy with ` .min ` only |
500+ | | Before | After |
501+ | -------------------------------------------------| -----------------------| --------------------------------------------------------|
502+ | Latitude type enforces pole constraint | ❌ user responsibility | ✅ compile-time, zero-overhead |
503+ | Longitude wraps cyclically | ❌ manual modulo | ✅ ` wrap_to_range ` on origin |
504+ | Sensor range clamped at API boundary | ❌ runtime if-else | ✅ policy on origin |
505+ | Relative origin inherits parent bounds | ❌ impossible | ✅ automatic, static-checked nesting |
506+ | ` min() ` /` max() ` /` numeric_limits ` reflect bounds | ❌ reports type limits | ✅ reports domain limits |
507+ | Half-line (non-negative) bounds | ❌ impossible | ✅ ` check_non_negative ` / ` clamp_non_negative ` policies |
508+ | Non-negative QS auto-guards natural origins | ❌ impossible | ✅ automatic, no specialization needed |
481509
482510The implementation is already merged and covered by a comprehensive compile-time
483511test suite. Documentation lives in the
0 commit comments