VLink  2.0.0
A high-performance communication middleware
quantize.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file quantize.h
26  * @brief Header-only helpers for linear integer quantisation and dequantisation.
27  *
28  * @details
29  * @c vlink::Quantize provides the small, deterministic numeric conversion used by compact
30  * containers such as @c zerocopy::PointCloud. It linearly maps a caller supplied real interval
31  * (@c quant_min, @c quant_max) to an integral storage interval and provides the inverse mapping
32  * for reads. The helpers are templates so the public API stays allocation-free, exception-free
33  * and independent of any particular point-cloud type.
34  *
35  * @par Mapping model
36  *
37  * | Step | Behaviour |
38  * | ---------- | ------------------------------------------------------------------------- |
39  * | Encode | Map @c value from @c [quant_min,quant_max] into @c QuantT storage. |
40  * | Decode | Map an integral storage value back with the same linear transform. |
41  * | Rounding | Round half away from zero before casting to the storage type. |
42  * | Type math | Use @c std::common_type_t<float,...> so all-integer inputs still divide in floating point. |
43  * | Bad range | Return zero when any input is NaN or @c quant_max <= @c quant_min. |
44  * | Saturation | Clamp encoded values to the storage limits when the scaled value overflows. |
45  *
46  * For signed storage types, the normal mapping interval is symmetric:
47  * @c [-std::numeric_limits<T>::max(),std::numeric_limits<T>::max()]. The most negative storage
48  * value, such as @c -32768 for @c int16_t, is reserved for negative saturation. It is accepted
49  * by @c decode, but because it sits one step below the normal interval it can decode slightly
50  * below @c quant_min. This keeps @c 0 centred for ranges like @c [-extent,+extent].
51  *
52  * @par Typical usage
53  * @code
54  * constexpr int extent = 100;
55  *
56  * int16_t qx = vlink::Quantize::encode<int16_t>(extent, x); // maps [-extent,+extent]
57  * float x_out = vlink::Quantize::decode<float>(extent, qx);
58  * @endcode
59  *
60  * @par Example
61  * @code
62  * int16_t stored = vlink::Quantize::encode<int16_t>(-10, 10, 1.25f);
63  * float value = vlink::Quantize::decode<float>(-10, 10, stored);
64  * @endcode
65  */
66 
67 #pragma once
68 
69 #include <limits>
70 #include <type_traits>
71 
72 #include "./macros.h"
73 
74 namespace vlink {
75 
76 /**
77  * @namespace vlink::Quantize
78  * @brief Stateless linear quantisation helpers.
79  */
80 namespace Quantize { // NOLINT(readability-identifier-naming)
81 
82 /**
83  * @brief Quantizes a value from a real range into an integral type.
84  *
85  * @param quant_min Minimum value of the real range.
86  * @param quant_max Maximum value of the real range.
87  * @param value Value to quantize.
88  * @return Quantized value.
89  */
90 template <typename QuantT, typename MinT, typename MaxT, typename ValueT>
91 [[nodiscard]] QuantT encode(MinT quant_min, MaxT quant_max, ValueT value) noexcept;
92 
93 /**
94  * @brief Dequantizes an integral value into a real range.
95  *
96  * @param quant_min Minimum value of the real range.
97  * @param quant_max Maximum value of the real range.
98  * @param value Quantized value.
99  * @return Dequantized value.
100  */
101 template <typename ReturnT, typename MinT, typename MaxT, typename ValueT>
102 [[nodiscard]] ReturnT decode(MinT quant_min, MaxT quant_max, ValueT value) noexcept;
103 
104 /**
105  * @brief Quantizes a value from @c [-extent,+extent] into a signed integral type.
106  *
107  * @details Equivalent to @c encode<QuantT>(-extent, extent, value), but avoids
108  * the generic min/max mapping setup for symmetric extent ranges.
109  *
110  * @param extent Positive symmetric extent.
111  * @param value Value to quantize.
112  * @return Quantized value, or @c 0 for invalid extent / NaN input.
113  */
114 template <typename QuantT, typename ExtentT, typename ValueT>
115 [[nodiscard]] QuantT encode(ExtentT extent, ValueT value) noexcept;
116 
117 /**
118  * @brief Dequantizes a signed integral value from @c [-extent,+extent].
119  *
120  * @details Equivalent to @c decode<ReturnT>(-extent, extent, value), but avoids
121  * the generic min/max mapping setup for symmetric extent ranges.
122  *
123  * @param extent Positive symmetric extent.
124  * @param value Quantized value.
125  * @return Dequantized value, or @c 0 for invalid extent.
126  */
127 template <typename ReturnT, typename ExtentT, typename ValueT>
128 [[nodiscard]] ReturnT decode(ExtentT extent, ValueT value) noexcept;
129 
130 ////////////////////////////////////////////////////////////////
131 /// Details
132 ////////////////////////////////////////////////////////////////
133 
134 template <typename QuantT, typename MinT, typename MaxT, typename ValueT>
135 inline QuantT encode(MinT quant_min, MaxT quant_max, ValueT value) noexcept {
136  static_assert(std::is_integral_v<QuantT> && !std::is_same_v<QuantT, bool>, "QuantT must be integral.");
137  static_assert(std::is_arithmetic_v<MinT>, "MinT must be arithmetic.");
138  static_assert(std::is_arithmetic_v<MaxT>, "MaxT must be arithmetic.");
139  static_assert(std::is_arithmetic_v<ValueT>, "ValueT must be arithmetic.");
140 
141  using CalcT = std::common_type_t<float, MinT, MaxT, ValueT>;
142 
143  auto min = static_cast<CalcT>(quant_min);
144  auto max = static_cast<CalcT>(quant_max);
145  auto target = static_cast<CalcT>(value);
146 
147  if VUNLIKELY (min != min || max != max || target != target || max <= min) {
148  return static_cast<QuantT>(0);
149  }
150 
151  auto quant_max_value = static_cast<CalcT>(std::numeric_limits<QuantT>::max());
152  auto quant_min_value =
153  std::is_signed_v<QuantT> ? -quant_max_value : static_cast<CalcT>(std::numeric_limits<QuantT>::lowest());
154 
155  auto quant_lowest_value = static_cast<CalcT>(std::numeric_limits<QuantT>::lowest());
156  auto quant_range = quant_max_value - quant_min_value;
157 
158  auto scaled = ((target - min) * quant_range / (max - min)) + quant_min_value;
159  auto rounded = scaled >= static_cast<CalcT>(0) ? scaled + static_cast<CalcT>(0.5) : scaled - static_cast<CalcT>(0.5);
160 
161  if VUNLIKELY (rounded >= quant_max_value) {
162  return std::numeric_limits<QuantT>::max();
163  }
164 
165  if VUNLIKELY (rounded <= quant_lowest_value) {
166  return std::numeric_limits<QuantT>::lowest();
167  }
168 
169  return static_cast<QuantT>(rounded);
170 }
171 
172 template <typename QuantT, typename ExtentT, typename ValueT>
173 inline QuantT encode(ExtentT extent, ValueT value) noexcept {
174  static_assert(std::is_integral_v<QuantT> && std::is_signed_v<QuantT> && !std::is_same_v<QuantT, bool>,
175  "QuantT must be a signed integral type.");
176  static_assert(std::is_arithmetic_v<ExtentT>, "ExtentT must be arithmetic.");
177  static_assert(std::is_arithmetic_v<ValueT>, "ValueT must be arithmetic.");
178 
179  using CalcT = std::common_type_t<float, ExtentT, ValueT>;
180 
181  auto extent_value = static_cast<CalcT>(extent);
182  auto target = static_cast<CalcT>(value);
183 
184  if VUNLIKELY (extent_value != extent_value || target != target || extent_value <= static_cast<CalcT>(0)) {
185  return static_cast<QuantT>(0);
186  }
187 
188  auto quant_max_value = static_cast<CalcT>(std::numeric_limits<QuantT>::max());
189  auto quant_lowest_value = static_cast<CalcT>(std::numeric_limits<QuantT>::lowest());
190  auto scaled = target * quant_max_value / extent_value;
191  auto rounded = scaled >= static_cast<CalcT>(0) ? scaled + static_cast<CalcT>(0.5) : scaled - static_cast<CalcT>(0.5);
192 
193  if VUNLIKELY (rounded >= quant_max_value) {
194  return std::numeric_limits<QuantT>::max();
195  }
196 
197  if VUNLIKELY (rounded <= quant_lowest_value) {
198  return std::numeric_limits<QuantT>::lowest();
199  }
200 
201  return static_cast<QuantT>(rounded);
202 }
203 
204 template <typename ReturnT, typename MinT, typename MaxT, typename ValueT>
205 inline ReturnT decode(MinT quant_min, MaxT quant_max, ValueT value) noexcept {
206  static_assert(std::is_arithmetic_v<ReturnT>, "ReturnT must be arithmetic.");
207  static_assert(std::is_arithmetic_v<MinT>, "MinT must be arithmetic.");
208  static_assert(std::is_arithmetic_v<MaxT>, "MaxT must be arithmetic.");
209  static_assert(std::is_integral_v<ValueT> && !std::is_same_v<ValueT, bool>, "ValueT must be integral.");
210 
211  using CalcT = std::common_type_t<float, ReturnT, MinT, MaxT>;
212 
213  auto min = static_cast<CalcT>(quant_min);
214  auto max = static_cast<CalcT>(quant_max);
215  auto target = static_cast<CalcT>(value);
216 
217  if VUNLIKELY (min != min || max != max || target != target || max <= min) {
218  return static_cast<ReturnT>(0);
219  }
220 
221  auto quant_max_value = static_cast<CalcT>(std::numeric_limits<ValueT>::max());
222  auto quant_min_value =
223  std::is_signed_v<ValueT> ? -quant_max_value : static_cast<CalcT>(std::numeric_limits<ValueT>::lowest());
224 
225  auto quant_lowest_value = static_cast<CalcT>(std::numeric_limits<ValueT>::lowest());
226  auto quant_range = quant_max_value - quant_min_value;
227 
228  if VUNLIKELY (target >= quant_max_value) {
229  target = quant_max_value;
230  } else if VUNLIKELY (target <= quant_lowest_value) {
231  target = quant_lowest_value;
232  }
233 
234  return static_cast<ReturnT>(((target - quant_min_value) * (max - min) / quant_range) + min);
235 }
236 
237 template <typename ReturnT, typename ExtentT, typename ValueT>
238 inline ReturnT decode(ExtentT extent, ValueT value) noexcept {
239  static_assert(std::is_arithmetic_v<ReturnT>, "ReturnT must be arithmetic.");
240  static_assert(std::is_arithmetic_v<ExtentT>, "ExtentT must be arithmetic.");
241  static_assert(std::is_integral_v<ValueT> && std::is_signed_v<ValueT> && !std::is_same_v<ValueT, bool>,
242  "ValueT must be a signed integral type.");
243 
244  using CalcT = std::common_type_t<float, ReturnT, ExtentT, ValueT>;
245 
246  auto extent_value = static_cast<CalcT>(extent);
247 
248  if VUNLIKELY (extent_value != extent_value || extent_value <= static_cast<CalcT>(0)) {
249  return static_cast<ReturnT>(0);
250  }
251 
252  auto target = static_cast<CalcT>(value);
253  auto quant_max_value = static_cast<CalcT>(std::numeric_limits<ValueT>::max());
254 
255  return static_cast<ReturnT>(target * extent_value / quant_max_value);
256 }
257 
258 } // namespace Quantize
259 
260 } // namespace vlink
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VUNLIKELY(...)
Short alias for VLINK_UNLIKELY.
Definition: macros.h:289