SDDS ToolKit Programs and Libraries for C and Python
Loading...
Searching...
No Matches
SDDSTransform.cc
Go to the documentation of this file.
1/**
2 * @file SDDSTransform.cc
3 * @brief Typed layout and page transformations for the C++17 SDDS interface.
4 *
5 * @details Implements checked scalar and vector conversions, layout editing,
6 * row masks and views, page filtering and projection, unit conversion, and
7 * streaming transformed copies.
8 *
9 * @copyright
10 * - (c) 2026 The University of Chicago
11 *
12 * @license
13 * This file is distributed under the terms of the Software License Agreement
14 * found in the file LICENSE included with this distribution.
15 */
16
17#include "SDDS.hpp"
18
19#include <algorithm>
20#include <cmath>
21#include <limits>
22#include <type_traits>
23
24namespace sdds {
25namespace {
26
27[[noreturn]] void typeError(const std::string &message,
28 std::optional<std::string> field = std::nullopt) {
29 throw TypeError(ErrorKind::Type, message, {}, 0, std::move(field));
30}
31
32[[noreturn]] void stateError(const std::string &message) {
33 throw StateError(ErrorKind::State, message);
34}
35
36template <class To, class From>
37To checkedIntegralCast(From value) {
38 static_assert(std::is_integral_v<To> && std::is_integral_v<From>);
39 if constexpr (std::is_signed_v<From> == std::is_signed_v<To>) {
40 if constexpr (sizeof(To) < sizeof(From)) {
41 if (value < static_cast<From>(std::numeric_limits<To>::lowest()) ||
42 value > static_cast<From>(std::numeric_limits<To>::max()))
43 typeError("numeric conversion is out of range");
44 }
45 } else if constexpr (std::is_signed_v<From>) {
46 if (value < 0)
47 typeError("negative value cannot be converted to an unsigned type");
48 using UnsignedFrom = std::make_unsigned_t<From>;
49 if constexpr (sizeof(To) < sizeof(UnsignedFrom))
50 if (static_cast<UnsignedFrom>(value) > std::numeric_limits<To>::max())
51 typeError("numeric conversion is out of range");
52 } else {
53 using UnsignedTo = std::make_unsigned_t<To>;
54 if constexpr (sizeof(To) <= sizeof(From))
55 if (value > static_cast<From>(static_cast<UnsignedTo>(std::numeric_limits<To>::max())))
56 typeError("numeric conversion is out of range");
57 }
58 return static_cast<To>(value);
59}
60
61long double rounded(long double value, RoundingMode mode) {
62 if (!std::isfinite(value))
63 typeError("non-finite value cannot be converted to an integer");
64 switch (mode) {
65 case RoundingMode::Reject:
66 if (std::trunc(value) != value)
67 typeError("floating-to-integer conversion requires an explicit rounding mode");
68 return value;
69 case RoundingMode::TowardZero: return std::trunc(value);
70 case RoundingMode::Nearest: return std::nearbyint(value);
71 case RoundingMode::Down: return std::floor(value);
72 case RoundingMode::Up: return std::ceil(value);
73 }
74 typeError("unknown rounding mode");
75}
76
77template <class To, class From>
78To checkedNumericCast(From value, RoundingMode rounding) {
79 static_assert(std::is_arithmetic_v<To> && std::is_arithmetic_v<From>);
80 if constexpr (std::is_integral_v<To> && std::is_integral_v<From>) {
81 return checkedIntegralCast<To>(value);
82 } else if constexpr (std::is_integral_v<To>) {
83 const long double converted = rounded(static_cast<long double>(value), rounding);
84 if (converted < static_cast<long double>(std::numeric_limits<To>::lowest()) ||
85 converted > static_cast<long double>(std::numeric_limits<To>::max()))
86 typeError("numeric conversion is out of range");
87 return static_cast<To>(converted);
88 } else {
89 const long double converted = static_cast<long double>(value);
90 if (std::isfinite(converted) &&
91 (converted < -static_cast<long double>(std::numeric_limits<To>::max()) ||
92 converted > static_cast<long double>(std::numeric_limits<To>::max())))
93 typeError("numeric conversion is out of range");
94 return static_cast<To>(value);
95 }
96}
97
98template <class To>
99Scalar convertTo(const Scalar &value, RoundingMode rounding) {
100 return std::visit([&](const auto &source) -> Scalar {
101 using From = std::decay_t<decltype(source)>;
102 if constexpr (std::is_same_v<From, std::string>) {
103 typeError("strings cannot be converted to numeric SDDS types");
104 } else {
105 return checkedNumericCast<To>(source, rounding);
106 }
107 }, value);
108}
109
110Scalar scalarAt(const Values &values, std::size_t index) {
111 return std::visit([&](const auto &items) -> Scalar { return items.at(index); }, values);
112}
113
114std::uint64_t valueCount(const Values &values) {
115 return std::visit([](const auto &items) {
116 return static_cast<std::uint64_t>(items.size());
117 }, values);
118}
119
120void appendScalar(Values &values, Scalar value) {
121 std::visit([&](auto &items) {
122 using Vector = std::decay_t<decltype(items)>;
123 using Value = typename Vector::value_type;
124 items.push_back(std::get<Value>(std::move(value)));
125 }, values);
126}
127
128Values emptyValues(Type type) {
129 switch (type) {
130 case Type::LongDouble: return std::vector<long double>{};
131 case Type::Double: return std::vector<double>{};
132 case Type::Float: return std::vector<float>{};
133 case Type::Int64: return std::vector<std::int64_t>{};
134 case Type::UInt64: return std::vector<std::uint64_t>{};
135 case Type::Int32: return std::vector<std::int32_t>{};
136 case Type::UInt32: return std::vector<std::uint32_t>{};
137 case Type::Int16: return std::vector<std::int16_t>{};
138 case Type::UInt16: return std::vector<std::uint16_t>{};
139 case Type::String: return std::vector<std::string>{};
140 case Type::Character: return std::vector<char>{};
141 }
142 typeError("unknown SDDS type");
143}
144
145template <class Definition>
146void renameDefinition(std::vector<Definition> &definitions, std::size_t index,
147 std::string newName) {
148 if (newName.empty())
149 typeError("definition names cannot be empty");
150 for (std::size_t existing = 0; existing < definitions.size(); ++existing)
151 if (existing != index && definitions[existing].name == newName)
152 typeError("duplicate definition name: " + newName, newName);
153 definitions.at(index).name = std::move(newName);
154}
155
156template <class Definition>
157void replaceDefinition(std::vector<Definition> &definitions, std::size_t index,
158 Definition definition) {
159 if (definition.name.empty())
160 typeError("definition names cannot be empty");
161 for (std::size_t existing = 0; existing < definitions.size(); ++existing)
162 if (existing != index && definitions[existing].name == definition.name)
163 typeError("duplicate definition name: " + definition.name, definition.name);
164 definitions.at(index) = std::move(definition);
165}
166
167template <class Definition>
168void eraseDefinition(std::vector<Definition> &definitions, std::size_t index) {
169 definitions.erase(definitions.begin() + static_cast<std::ptrdiff_t>(index));
170}
171
172bool selectedRow(const RowSlice &slice, std::int64_t row, std::int64_t total) {
173 if (slice.first < 0 || slice.stride < 1 || (slice.count && *slice.count < 0) ||
174 (slice.last && *slice.last < 0) || (slice.last && (slice.first || slice.count)))
175 stateError("invalid row slice");
176 const std::int64_t begin = slice.last ? std::max<std::int64_t>(0, total - *slice.last)
177 : std::min(slice.first, total);
178 if (row < begin || (row - begin) % slice.stride)
179 return false;
180 if (!slice.count)
181 return true;
182 return (row - begin) / slice.stride < *slice.count;
183}
184
185std::vector<bool> selectedFields(const FieldSelection &selection,
186 const std::vector<std::string> &names) {
187 std::vector<bool> result(names.size(), selection.all);
188 if (selection.all && selection.names.empty())
189 return result;
190 if (selection.all)
191 stateError("an all-fields selection cannot also list field names");
192 for (const auto &name : selection.names) {
193 const auto found = std::find(names.begin(), names.end(), name);
194 if (found == names.end())
195 typeError("unknown projected field: " + name, name);
196 result[static_cast<std::size_t>(found - names.begin())] = true;
197 }
198 return result;
199}
200
201template <class Definition>
202std::vector<std::string> namesOf(const std::vector<Definition> &definitions) {
203 std::vector<std::string> names;
204 names.reserve(definitions.size());
205 for (const auto &definition : definitions)
206 names.push_back(definition.name);
207 return names;
208}
209
210Scalar scaledScalar(const Scalar &value, Type type, long double factor) {
211 if (type == Type::String)
212 typeError("unit conversion requires numeric data");
213 const long double source = std::visit([](const auto &item) -> long double {
214 using Value = std::decay_t<decltype(item)>;
215 if constexpr (std::is_same_v<Value, std::string>)
216 typeError("unit conversion requires numeric data");
217 else
218 return static_cast<long double>(item);
219 }, value);
220 return convertScalar(Scalar(source * factor), type, RoundingMode::Reject);
221}
222
223Values scaledValues(const Values &values, Type type, long double factor) {
224 Values result = emptyValues(type);
225 const std::size_t count = std::visit([](const auto &items) { return items.size(); }, values);
226 for (std::size_t index = 0; index < count; ++index)
227 appendScalar(result, scaledScalar(scalarAt(values, index), type, factor));
228 return result;
229}
230
231} // namespace
232
233Scalar convertScalar(const Scalar &value, Type target, RoundingMode rounding) {
234 if (target == Type::String) {
235 if (const auto *text = std::get_if<std::string>(&value))
236 return *text;
237 typeError("numeric values cannot be implicitly converted to strings");
238 }
239 switch (target) {
240 case Type::LongDouble: return convertTo<long double>(value, rounding);
241 case Type::Double: return convertTo<double>(value, rounding);
242 case Type::Float: return convertTo<float>(value, rounding);
243 case Type::Int64: return convertTo<std::int64_t>(value, rounding);
244 case Type::UInt64: return convertTo<std::uint64_t>(value, rounding);
245 case Type::Int32: return convertTo<std::int32_t>(value, rounding);
246 case Type::UInt32: return convertTo<std::uint32_t>(value, rounding);
247 case Type::Int16: return convertTo<std::int16_t>(value, rounding);
248 case Type::UInt16: return convertTo<std::uint16_t>(value, rounding);
249 case Type::Character: return convertTo<char>(value, rounding);
250 case Type::String: break;
251 }
252 typeError("unknown conversion target");
253}
254
255Values convertValues(const Values &values, Type target, RoundingMode rounding) {
256 if (typeOf(values) == target)
257 return values;
258 Values result = emptyValues(target);
259 const std::size_t count = std::visit([](const auto &items) { return items.size(); }, values);
260 for (std::size_t index = 0; index < count; ++index)
261 appendScalar(result, convertScalar(scalarAt(values, index), target, rounding));
262 return result;
263}
264
265LayoutEditor::LayoutEditor(Layout layout) : layout_(std::move(layout)) {}
266
267LayoutEditor &LayoutEditor::renameParameter(std::string_view name, std::string newName) {
268 renameDefinition(layout_.parameters, layout_.parameterIndex(name), std::move(newName));
269 return *this;
270}
271LayoutEditor &LayoutEditor::renameArray(std::string_view name, std::string newName) {
272 renameDefinition(layout_.arrays, layout_.arrayIndex(name), std::move(newName));
273 return *this;
274}
275LayoutEditor &LayoutEditor::renameColumn(std::string_view name, std::string newName) {
276 renameDefinition(layout_.columns, layout_.columnIndex(name), std::move(newName));
277 return *this;
278}
279LayoutEditor &LayoutEditor::renameAssociate(std::string_view name, std::string newName) {
280 renameDefinition(layout_.associates, layout_.associateIndex(name), std::move(newName));
281 return *this;
282}
283LayoutEditor &LayoutEditor::dropParameter(std::string_view name) {
284 eraseDefinition(layout_.parameters, layout_.parameterIndex(name)); return *this;
285}
286LayoutEditor &LayoutEditor::dropArray(std::string_view name) {
287 eraseDefinition(layout_.arrays, layout_.arrayIndex(name)); return *this;
288}
289LayoutEditor &LayoutEditor::dropColumn(std::string_view name) {
290 eraseDefinition(layout_.columns, layout_.columnIndex(name)); return *this;
291}
292LayoutEditor &LayoutEditor::dropAssociate(std::string_view name) {
293 eraseDefinition(layout_.associates, layout_.associateIndex(name)); return *this;
294}
295LayoutEditor &LayoutEditor::replaceParameter(std::string_view name,
296 ParameterDefinition definition) {
297 replaceDefinition(layout_.parameters, layout_.parameterIndex(name), std::move(definition));
298 return *this;
299}
300LayoutEditor &LayoutEditor::replaceArray(std::string_view name, ArrayDefinition definition) {
301 replaceDefinition(layout_.arrays, layout_.arrayIndex(name), std::move(definition));
302 return *this;
303}
304LayoutEditor &LayoutEditor::replaceColumn(std::string_view name, ColumnDefinition definition) {
305 replaceDefinition(layout_.columns, layout_.columnIndex(name), std::move(definition));
306 return *this;
307}
308LayoutEditor &LayoutEditor::replaceAssociate(std::string_view name,
309 AssociateDefinition definition) {
310 replaceDefinition(layout_.associates, layout_.associateIndex(name), std::move(definition));
311 return *this;
312}
313LayoutEditor &LayoutEditor::addParameter(ParameterDefinition definition) {
314 layout_ = LayoutBuilder(layout_).addParameter(std::move(definition)).build(); return *this;
315}
316LayoutEditor &LayoutEditor::addArray(ArrayDefinition definition) {
317 layout_ = LayoutBuilder(layout_).addArray(std::move(definition)).build(); return *this;
318}
319LayoutEditor &LayoutEditor::addColumn(ColumnDefinition definition) {
320 layout_ = LayoutBuilder(layout_).addColumn(std::move(definition)).build(); return *this;
321}
322LayoutEditor &LayoutEditor::addAssociate(AssociateDefinition definition) {
323 layout_ = LayoutBuilder(layout_).addAssociate(std::move(definition)).build(); return *this;
324}
325Layout LayoutEditor::build() const { return LayoutBuilder(layout_).build(); }
326
327RowMask::RowMask(std::size_t rows, bool selected) : selected_(rows, selected ? 1U : 0U) {}
328std::size_t RowMask::count() const noexcept {
329 return static_cast<std::size_t>(std::count(selected_.begin(), selected_.end(), 1U));
330}
331bool RowMask::test(std::size_t row) const { return selected_.at(row) != 0; }
332void RowMask::set(std::size_t row, bool selected) { selected_.at(row) = selected ? 1U : 0U; }
333RowMask &RowMask::operator&=(const RowMask &other) {
334 if (size() != other.size()) stateError("row masks have different sizes");
335 for (std::size_t row = 0; row < size(); ++row) selected_[row] &= other.selected_[row];
336 return *this;
337}
338RowMask &RowMask::operator|=(const RowMask &other) {
339 if (size() != other.size()) stateError("row masks have different sizes");
340 for (std::size_t row = 0; row < size(); ++row) selected_[row] |= other.selected_[row];
341 return *this;
342}
343RowMask RowMask::operator~() const {
344 RowMask result = *this;
345 for (auto &selected : result.selected_) selected = selected ? 0U : 1U;
346 return result;
347}
348
349Scalar RowView::value(std::size_t column) const {
350 if (!page_) stateError("row view has no page");
351 if (row_ < 0 || row_ >= page_->rowCount()) stateError("row index is out of range");
352 return scalarAt(page_->column(column), static_cast<std::size_t>(row_));
353}
354Scalar RowView::value(std::string_view column) const {
355 return value(page_->layout().columnIndex(column));
356}
357
358RowView Page::row(std::int64_t index) const {
359 if (index < 0 || index >= rowCount_) stateError("row index is out of range");
360 return RowView(this, index);
361}
362
363RowMask Page::matchRows(std::string_view name,
364 const std::function<bool(const Scalar &)> &predicate) const {
365 if (!predicate) stateError("row predicate is empty");
366 const Values &values = column(name);
367 RowMask result(static_cast<std::size_t>(rowCount_));
368 for (std::size_t rowIndex = 0; rowIndex < static_cast<std::size_t>(rowCount_); ++rowIndex)
369 result.set(rowIndex, predicate(scalarAt(values, rowIndex)));
370 return result;
371}
372
373Page Page::filtered(const RowMask &mask) const {
374 if (mask.size() != static_cast<std::size_t>(rowCount_))
375 stateError("row mask size does not match the page");
376 Page result(layout_, LoadMode::None);
377 result.maxTransformationElements_ = maxTransformationElements_;
378 result.number_ = number_;
379 result.recovered_ = recovered_;
380 std::uint64_t elements = 0;
381 const auto addElements = [&](std::uint64_t amount) {
382 if (amount > maxTransformationElements_ - elements)
383 throw LimitError(ErrorKind::Limit,
384 "page filtering exceeds transformation output limit");
385 elements += amount;
386 };
387 for (std::size_t index = 0; index < parameters_.size(); ++index)
388 if (parameterLoaded(index)) {
389 addElements(1);
390 result.setParameter(index, parameters_[index]);
391 }
392 for (std::size_t index = 0; index < arrays_.size(); ++index)
393 if (arrayLoaded(index)) {
394 addElements(valueCount(arrays_[index].values));
395 result.setArray(index, arrays_[index]);
396 }
397 for (std::size_t columnIndex = 0; columnIndex < columns_.size(); ++columnIndex) {
398 if (!columnLoaded(columnIndex)) continue;
399 addElements(static_cast<std::uint64_t>(mask.count()));
400 Values selected = emptyValues(layout().columns[columnIndex].type);
401 for (std::size_t rowIndex = 0; rowIndex < mask.size(); ++rowIndex)
402 if (mask.test(rowIndex)) appendScalar(selected, scalarAt(columns_[columnIndex], rowIndex));
403 result.setColumn(columnIndex, std::move(selected));
404 }
405 result.rowCount_ = static_cast<std::int64_t>(mask.count());
406 return result;
407}
408
409Page Page::projected(const ReadRequest &request) const {
410 const auto parameterSelection = selectedFields(request.parameters, namesOf(layout().parameters));
411 const auto arraySelection = selectedFields(request.arrays, namesOf(layout().arrays));
412 const auto columnSelection = selectedFields(request.columns, namesOf(layout().columns));
413 Page result(layout_, LoadMode::None);
414 result.maxTransformationElements_ = maxTransformationElements_;
415 result.number_ = number_;
416 result.recovered_ = recovered_;
417 for (std::size_t index = 0; index < parameters_.size(); ++index)
418 if (parameterSelection[index] && parameterLoaded(index)) result.setParameter(index, parameters_[index]);
419 for (std::size_t index = 0; index < arrays_.size(); ++index)
420 if (arraySelection[index] && arrayLoaded(index)) result.setArray(index, arrays_[index]);
421 for (std::size_t index = 0; index < columns_.size(); ++index)
422 if (columnSelection[index] && columnLoaded(index)) result.setColumn(index, columns_[index]);
423 RowMask rows(static_cast<std::size_t>(rowCount_));
424 for (std::int64_t index = 0; index < rowCount_; ++index)
425 rows.set(static_cast<std::size_t>(index), selectedRow(request.rows, index, rowCount_));
426 result.rowCount_ = rowCount_;
427 return result.filtered(rows);
428}
429
430Page convertUnits(const Page &page, FieldKind kind, std::string_view name,
431 std::optional<std::string> units, long double factor) {
432 Layout changed = page.layout();
433 if (!std::isfinite(factor)) typeError("unit conversion factor must be finite");
434 switch (kind) {
435 case FieldKind::Parameter: changed.parameters.at(changed.parameterIndex(name)).units = units; break;
436 case FieldKind::Array: changed.arrays.at(changed.arrayIndex(name)).units = units; break;
437 case FieldKind::Column: changed.columns.at(changed.columnIndex(name)).units = units; break;
438 }
439 auto layout = std::make_shared<const Layout>(LayoutBuilder(std::move(changed)).build());
440 Page result(layout, LoadMode::None);
441 result.maxTransformationElements_ = page.maxTransformationElements_;
442 result.number_ = page.number_;
443 result.recovered_ = page.recovered_;
444 result.rowCount_ = page.rowCount_;
445 for (std::size_t index = 0; index < page.parameters_.size(); ++index) {
446 if (!page.parameterLoaded(index)) continue;
447 Scalar value = page.parameters_[index];
448 if (kind == FieldKind::Parameter && page.layout().parameters[index].name == name)
449 value = scaledScalar(value, page.layout().parameters[index].type, factor);
450 result.setParameter(index, std::move(value));
451 }
452 for (std::size_t index = 0; index < page.arrays_.size(); ++index) {
453 if (!page.arrayLoaded(index)) continue;
454 ArrayData value = page.arrays_[index];
455 if (kind == FieldKind::Array && page.layout().arrays[index].name == name)
456 value.values = scaledValues(value.values, page.layout().arrays[index].type, factor);
457 result.setArray(index, std::move(value));
458 }
459 for (std::size_t index = 0; index < page.columns_.size(); ++index) {
460 if (!page.columnLoaded(index)) continue;
461 Values value = page.columns_[index];
462 if (kind == FieldKind::Column && page.layout().columns[index].name == name)
463 value = scaledValues(value, page.layout().columns[index].type, factor);
464 result.setColumn(index, std::move(value));
465 }
466 result.rowCount_ = page.rowCount_;
467 return result;
468}
469
470void copyDataset(Reader &reader, const std::filesystem::path &output, CopyOptions options) {
471 Layout outputLayout = options.transformLayout ? options.transformLayout(reader.layout())
472 : reader.layout();
473 auto shared = std::make_shared<const Layout>(outputLayout);
474 Writer writer = Writer::create(output, outputLayout, options.writer);
475 while (auto page = reader.next(options.read)) {
476 if (options.transformPage) {
477 auto transformed = options.transformPage(std::move(*page), shared);
478 if (transformed) writer.write(std::move(*transformed));
479 } else {
480 writer.write(std::move(*page));
481 }
482 }
483 writer.close();
484}
485
486} // namespace sdds