Skip to content

Commit 10b4fcd

Browse files
mpuszburnpanck
andauthored
fix: improve efficiency and accuracy of value_cast for quantity_point (#765)
Rewrite the quantity_point overload of sudo_cast to pick the best intermediate unit/rep for each conversion case: - Same origin: delegate entirely to the quantity sudo_cast (unchanged). Also fix a pre-existing bug where the result was incorrectly tagged with FromQP::point_origin instead of ToQP::point_origin. - Same unit, different origin: skip unit scaling altogether; compute the origin offset in the common rep and add it directly. - Different unit, floating-point rep: pick the larger of the two units as the intermediate (to minimise the scaling magnitude), then use point_for() to let the library's common-unit arithmetic handle the origin shift — this avoids the binary FP precision loss that occurs when the offset is expressed explicitly in the larger unit. - Different unit, integer rep: prefer the output unit as intermediate so no extra scaling is needed after the offset addition. If the origin offset would overflow the output-unit type, fall back to the input unit where it is guaranteed to fit. Also expand the value_cast tests for quantity_point to cover every branch explicitly, including the new integer overflow-fallback path. Closes: #598 Co-authored-by: Yves Delley <burnpanck@users.noreply.github.com>
1 parent efbc844 commit 10b4fcd

File tree

2 files changed

+118
-27
lines changed

2 files changed

+118
-27
lines changed

src/core/include/mp-units/bits/sudo_cast.h

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -121,40 +121,99 @@ template<QuantityPoint ToQP, typename FwdFromQP, QuantityPoint FromQP = std::rem
121121
[[nodiscard]] constexpr QuantityPoint auto sudo_cast(FwdFromQP&& qp)
122122
{
123123
if constexpr (is_same_v<MP_UNITS_NONCONST_TYPE(ToQP::point_origin), MP_UNITS_NONCONST_TYPE(FromQP::point_origin)>) {
124+
// Same origin: delegate entirely to the quantity sudo_cast — no offset arithmetic needed.
124125
return quantity_point{
125126
sudo_cast<typename ToQP::quantity_type>(std::forward<FwdFromQP>(qp).quantity_from(FromQP::point_origin)),
126-
FromQP::point_origin};
127+
ToQP::point_origin};
127128
} else {
128-
// it's unclear how hard we should try to avoid truncation here. For now, the only corner case we cater for,
129-
// is when the range of the quantity type of at most one of QP or ToQP doesn't cover the offset between the
130-
// point origins. In that case, we need to be careful to ensure we use the quantity type with the larger range
131-
// of the two to perform the point_origin conversion.
129+
// Different origins: we need to (a) convert the rep/unit, (b) add the origin offset.
130+
// The order and intermediate unit choice matters for accuracy and overflow avoidance.
131+
//
132+
// Strategy: pick an intermediate unit/rep, then compute:
133+
// result = sudo_cast<intermediate>(input_quantity) + offset
134+
// where offset is the static difference between the two origins expressed in the
135+
// intermediate unit. Finally, sudo_cast the sum to the target type.
136+
//
132137
// Numerically, we'll potentially need to do three things:
133138
// (a) cast the representation type
134139
// (b) scale the numerical value
135140
// (c) add/subtract the origin difference
136-
// In the following, we carefully select the order of these three operations: each of (a) and (b) is scheduled
137-
// either before or after (c), such that (c) acts on the largest range possible among all combination of source
138-
// and target unit and representation.
141+
// The intermediate unit determines the order of (b) and (c).
142+
139143
constexpr UnitMagnitude auto c_mag =
140144
mp_units::get_canonical_unit(FromQP::unit).mag / mp_units::get_canonical_unit(ToQP::unit).mag;
141145
using type_traits = conversion_type_traits<c_mag, typename FromQP::rep, typename ToQP::rep>;
146+
using c_rep_type = type_traits::c_rep_type;
142147
using c_type = type_traits::c_type;
143-
if constexpr (get_value<long double>(c_mag) > 1.) {
144-
// original unit had a larger unit magnitude; if we first convert to the common representation but retain the
145-
// unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal
146-
// for the offset computation.
147-
return sudo_cast<ToQP>(
148-
sudo_cast<quantity_point<FromQP::reference, FromQP::point_origin, c_type>>(std::forward<FwdFromQP>(qp))
149-
.point_for(ToQP::point_origin));
148+
149+
// Helper: statically compute the origin offset expressed in the given quantity type Q.
150+
// We go via quantity_point subtraction to handle all origin relationships correctly,
151+
// including cases where two zeroth_point_origins of different units exist.
152+
constexpr auto offset_as = [&]<typename Q>(std::type_identity<Q>) {
153+
constexpr auto zero = typename Q::rep{0} * Q::reference;
154+
return sudo_cast<Q>(quantity_point{zero, FromQP::point_origin} - quantity_point{zero, ToQP::point_origin});
155+
};
156+
157+
constexpr auto output_unit_ref = make_reference(FromQP::quantity_spec, ToQP::unit);
158+
159+
if constexpr (equivalent(FromQP::unit, ToQP::unit)) {
160+
// Same unit, different origin: no scaling needed — add the offset directly in the
161+
// common rep without any unit conversion. This is always exact.
162+
using intermediate_type = quantity<FromQP::reference, c_rep_type>;
163+
constexpr auto offset = offset_as(std::type_identity<intermediate_type>{});
164+
return quantity_point{
165+
sudo_cast<typename ToQP::quantity_type>(
166+
sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from(FromQP::point_origin)) + offset),
167+
ToQP::point_origin};
168+
} else if constexpr (treat_as_floating_point<c_type>) {
169+
// Floating-point intermediate: prefer the larger unit to minimise the magnitude of scaling
170+
// applied to the input value, then use point_for to let the library's common-unit quantity
171+
// arithmetic handle the origin offset (e.g. 2.0 km + 42 m → 2042.0 m, not 2.0 + 0.042 km).
172+
// This avoids the FP precision loss that would occur if we expressed the offset explicitly
173+
// in the intermediate unit.
174+
constexpr auto intermediate_ref = [&]() {
175+
if constexpr (!is_integral(pow<-1>(c_mag)))
176+
return FromQP::reference; // from-unit is larger
177+
else
178+
return output_unit_ref; // to-unit is larger (or equal)
179+
}();
180+
using intermediate_type = quantity<intermediate_ref, c_type>;
181+
return quantity_point{
182+
sudo_cast<typename ToQP::quantity_type>(
183+
quantity_point{sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from(FromQP::point_origin)),
184+
FromQP::point_origin}
185+
.point_for(ToQP::point_origin)
186+
.quantity_from(ToQP::point_origin)),
187+
ToQP::point_origin};
150188
} else {
151-
// new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing
152-
// truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two
153-
// representation types. Then, we can perform the offset computation.
154-
return sudo_cast<ToQP>(
155-
sudo_cast<quantity_point<make_reference(FromQP::quantity_spec, ToQP::unit), FromQP::point_origin, c_type>>(
156-
std::forward<FwdFromQP>(qp))
157-
.point_for(ToQP::point_origin));
189+
// Integer intermediate: use the output unit so the result is already in the right
190+
// unit before add, giving the most-accurate truncation. However, if the offset
191+
// would overflow in the output unit (e.g. because it was defined in a larger one),
192+
// fall back to the input unit.
193+
//
194+
// Detect overflow by computing the offset in long double expressed in the output unit
195+
// and comparing against the representable range of c_rep_type.
196+
constexpr long double offset_in_output_ld =
197+
offset_as(std::type_identity<quantity<output_unit_ref, long double>>{}).numerical_value_in(ToQP::unit);
198+
constexpr bool offset_fits_in_output =
199+
offset_in_output_ld >= static_cast<long double>(std::numeric_limits<c_rep_type>::min()) &&
200+
offset_in_output_ld <= static_cast<long double>(std::numeric_limits<c_rep_type>::max());
201+
if constexpr (offset_fits_in_output) {
202+
using intermediate_type = quantity<output_unit_ref, c_rep_type>;
203+
constexpr auto offset = offset_as(std::type_identity<intermediate_type>{});
204+
return quantity_point{
205+
sudo_cast<typename ToQP::quantity_type>(
206+
sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from(FromQP::point_origin)) + offset),
207+
ToQP::point_origin};
208+
} else {
209+
// offset overflows in the output unit — use the input unit instead
210+
using intermediate_type = quantity<FromQP::reference, c_rep_type>;
211+
constexpr auto offset = offset_as(std::type_identity<intermediate_type>{});
212+
return quantity_point{
213+
sudo_cast<typename ToQP::quantity_type>(
214+
sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from(FromQP::point_origin)) + offset),
215+
ToQP::point_origin};
216+
}
158217
}
159218
}
160219
}

test/static/quantity_point_test.cpp

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,27 +1789,59 @@ static_assert(value_cast_is_forbidden<quantity_point<m>, quantity_point<isq::wid
17891789
"value_cast shall not cast between different quantity types");
17901790
static_assert(value_cast_is_forbidden<quantity_point<isq::width[m]>, quantity_point<m>>(),
17911791
"value_cast shall not cast between different quantity types");
1792-
// value_cast which does not touch the point_origin
1792+
// value_cast which does not touch the point_origin (branch 1: same origin, delegates to quantity sudo_cast)
17931793
static_assert(value_cast<quantity_point<isq::height[m]>>(quantity_point{2 * isq::height[km]})
17941794
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2000);
17951795
static_assert(value_cast<quantity_point<isq::height[km]>>(quantity_point{2000 * isq::height[m]})
17961796
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(km) == 2);
1797-
// a value_cast which includes a change to the point origin
1797+
1798+
// value_cast which changes only the point origin, same unit (branch 2: no unit scaling, pure origin shift)
1799+
// -- floating-point intermediate (int input, default double output)
17981800
static_assert(value_cast<quantity_point<isq::height[m], mean_sea_level>>(quantity_point{2000 * isq::height[m],
17991801
ground_level})
18001802
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042);
1801-
// a value_cast which includes a change to the point origin as-well as a change in units
1803+
// -- integer intermediate: both reps are int, no promotion to double
1804+
static_assert(value_cast<quantity_point<isq::height[m], mean_sea_level, int>>(quantity_point{int{2000} * isq::height[m],
1805+
ground_level})
1806+
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042);
1807+
static_assert(value_cast<quantity_point<isq::height[m], ground_level, int>>(quantity_point{int{2042} * isq::height[m],
1808+
mean_sea_level})
1809+
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2000);
1810+
// -- double intermediate with a fractional value
1811+
static_assert(value_cast<quantity_point<isq::height[m], mean_sea_level, double>>(quantity_point{2000.5 * isq::height[m],
1812+
ground_level})
1813+
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042.5);
1814+
1815+
// value_cast which changes both unit and point origin with a floating-point intermediate
1816+
// (branch 3: point_for path avoids FP precision loss when computing the offset in the intermediate unit)
1817+
// -- from-unit is larger (km→m): intermediate stays in km, then point_for shifts to MSL
18021818
static_assert(value_cast<quantity_point<isq::height[m], mean_sea_level>>(quantity_point{2 * isq::height[km],
18031819
ground_level})
18041820
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042);
1805-
// a value_cast which changes all three of unit, rep, point_origin simultaneously, and the range of either FromQP or
1806-
// ToQP does not include the other's point_origin
1821+
// -- to-unit is larger (m→km): intermediate moves to km; 2000.0 m → 2.0 km, then +42 m → 2042.0 m → 2.042 km
1822+
static_assert(value_cast<quantity_point<isq::height[km], mean_sea_level>>(quantity_point{2000.0 * isq::height[m],
1823+
ground_level})
1824+
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(km) == 2.042);
1825+
1826+
// value_cast which changes unit, rep, and point origin with an integer intermediate (branches 4 & 5)
1827+
// -- branch 4: offset fits in output unit → use output unit as intermediate
1828+
// c_rep_type = common_type<int8_t, int> = int; offset = +42 m = 4200 cm fits in int
1829+
// 100 mm from ground_level = 10 cm; 10 cm + 4200 cm = 4210 cm from MSL
18071830
static_assert(value_cast<quantity_point<isq::height[cm], mean_sea_level, int>>(
18081831
quantity_point{std::int8_t{100} * isq::height[mm], ground_level})
18091832
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(cm) == 4210);
1833+
// -- branch 4 reversed: c_rep_type = common_type<int, int8_t> = int; offset = −42 m = −42000 mm fits in int
1834+
// 4210 cm from MSL = 42100 mm; 42100 mm + (−42000 mm) = 100 mm from ground_level
18101835
static_assert(value_cast<quantity_point<isq::height[mm], ground_level, std::int8_t>>(
18111836
quantity_point{4210 * isq::height[cm], mean_sea_level})
18121837
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(mm) == 100);
1838+
// -- branch 5: offset overflows output unit → fall back to input unit
1839+
// c_rep_type = common_type<int16_t, int16_t> = int16_t (range −32768..32767)
1840+
// offset in mm (output unit) = −42000 mm < −32768 → overflows int16_t → fall back to m (input unit)
1841+
// offset in m = −42 fits in int16_t; 43 m + (−42 m) = 1 m from ground_level = 1000 mm
1842+
static_assert(value_cast<quantity_point<isq::height[mm], ground_level, std::int16_t>>(
1843+
quantity_point{std::int16_t{43} * isq::height[m], mean_sea_level})
1844+
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(mm) == 1000);
18131845

18141846
//////////////////
18151847
// explicit conversion

0 commit comments

Comments
 (0)