NumericAL
A header-only C++20 lexer built on REAL
Loading...
Searching...
No Matches
tensor_ops.hpp
Go to the documentation of this file.
1
11#ifndef NUMERICAL_TENSOR_OPS_HPP
12#define NUMERICAL_TENSOR_OPS_HPP
13
14#include <algorithm>
15#include <array>
16#include <bit>
17#include <cmath>
18#include <complex>
19#include <concepts>
20#include <cstddef>
21#include <cstring>
22#include <functional>
23#include <limits>
24#include <string>
25#include <type_traits>
26#include <utility>
27#include <vector>
28
29#include "simd.hpp"
30#include "tensor.hpp"
31
32namespace numerical {
33
34 namespace detail {
35
37 [[nodiscard]] inline std::string shape_text(const std::vector<std::size_t>& shape)
38 {
39 std::string out {"("};
40 for (std::size_t i {0}; i < shape.size(); ++i) {
41 out += (i == 0 ? "" : ", ") + std::to_string(shape[i]);
42 }
43 return out + (shape.size() == 1 ? ",)" : ")");
44 }
45
48 [[nodiscard]] inline std::vector<std::size_t> broadcast_strides(const std::vector<std::size_t>& shape,
49 std::size_t rank)
50 {
51 std::vector<std::size_t> strides(rank, 0);
52 std::size_t stride {1};
53 for (std::size_t i {shape.size()}; i-- > 0;) {
54 strides[rank - shape.size() + i] = shape[i] == 1 ? 0 : stride;
55 stride *= shape[i];
56 }
57 return strides;
58 }
59
62 struct axis_walk
63 {
64 std::size_t outer;
65 std::size_t extent;
66 std::size_t inner;
67 };
68
71 [[nodiscard]] inline axis_walk walk_along(const std::vector<std::size_t>& shape,
72 std::size_t axis)
73 {
74 if (axis >= shape.size()) {
75 throw dimension_error("axis " + std::to_string(axis) + " is out of bounds for a tensor of rank "
76 + std::to_string(shape.size()));
77 }
78 const auto before {shape.begin() + static_cast<std::ptrdiff_t>(axis)};
79 axis_walk walk {.outer = 1, .extent = shape[axis], .inner = 1};
80 // The result's elements, the axis taken out: none (and no line walked) where a kept extent is 0, whatever
81 // the others; else their product, checked, which the outer and inner parts then divide.
82 if (std::find(shape.begin(), before, std::size_t {0}) != before || std::find(before + 1, shape.end(), std::size_t {0}) != shape.end()) {
83 walk.outer = 0;
84 walk.inner = 0;
85 return walk;
86 }
87 walk.outer = detail::element_count(shape.begin(), before);
88 walk.inner = detail::element_count(before + 1, shape.end());
89 static_cast<void>(detail::checked_mul(walk.outer, walk.inner));
90 return walk;
91 }
92
95 template <std::floating_point R>
97 R y,
98 R z,
99 bool fused)
100 {
101 if (fused) {
102 return std::fma(x, y, z);
103 }
104 const R product {x * y};
105 return product + z;
106 }
107
115 template <split_complex C>
117 const C& b,
118 bool fused)
119 {
120 using R = typename C::value_type;
121 const R ii {a.imag() * b.imag()};
122 const R ir {a.imag() * b.real()};
123 return C {numpy_muladd(a.real(), b.real(), -ii, fused), numpy_muladd(a.real(), b.imag(), ir, fused)};
124 }
125
131 template <split_complex C>
132 [[nodiscard]] C numpy_divide(const C& a,
133 const C& b,
134 bool fused)
135 {
136 using R = typename C::value_type;
137 const R real_part {std::abs(b.real())};
138 const R imaginary_part {std::abs(b.imag())};
139 if (real_part >= imaginary_part) {
140 if (real_part == R {0}) { // the imaginary part's magnitude no larger: a zero too
141 return C {a.real() / real_part, a.imag() / imaginary_part};
142 }
143 const R ratio {b.imag() / b.real()};
144 const R scale {R {1} / numpy_muladd(b.imag(), ratio, b.real(), fused)};
145 return C {numpy_muladd(a.imag(), ratio, a.real(), fused) * scale, numpy_muladd(-a.real(), ratio, a.imag(), fused) * scale};
146 }
147 const R ratio {b.real() / b.imag()};
148 const R scale {R {1} / numpy_muladd(b.real(), ratio, b.imag(), fused)};
149 return C {numpy_muladd(a.real(), ratio, a.imag(), fused) * scale, numpy_muladd(a.imag(), ratio, -a.real(), fused) * scale};
150 }
151
155 {
156#if defined(__aarch64__) || defined(_M_ARM64)
157 return true;
158#elif defined(NUMERICAL_X86_COMPILED)
159 return cpu_features().avx2_fma;
160#else
161 return false;
162#endif
163 }
164
169 {
170#if defined(__APPLE__) || defined(_WIN32)
171 return numpy_fuses_products();
172#else
173 return false;
174#endif
175 }
176
181 {
182#if defined(__APPLE__) && defined(__aarch64__)
183 return true;
184#else
185 return false;
186#endif
187 }
188
194 template <typename T>
196 const tensor<T>& b) noexcept
197 {
198 const auto ones {[](const tensor<T>& t) { return std::ranges::all_of(t.shape(), [](std::size_t e) { return e == 1; }); }};
199 const std::size_t rank {std::max(a.rank(), b.rank())};
200 const auto broadcast {[rank](const tensor<T>& t) { return t.rank() != 0 && t.rank() != rank; }};
201 if (!ones(a) || !ones(b)) {
202 return false;
203 }
204 return broadcast(a) || broadcast(b);
205 }
206
212 template <std::floating_point T>
214 std::size_t n)
215 {
216 using wide = std::common_type_t<T, double>;
217 return static_cast<T>(static_cast<wide>(value) / static_cast<wide>(n));
218 }
219
225 template <split_complex T>
226 [[nodiscard]] T divided_by_count(T value,
227 std::size_t n)
228 {
229 using R = typename T::value_type;
230 const std::complex<double> wide {static_cast<double>(value.real()), static_cast<double>(value.imag())};
231 const std::complex<double> mean {numpy_divide(wide, std::complex<double> {static_cast<double>(n), 0.0}, numpy_fuses_quotients())};
232 return T {static_cast<R>(mean.real()), static_cast<R>(mean.imag())};
233 }
234
237 template <typename T>
238 [[nodiscard]] T divided_by_count(T value,
239 std::size_t n)
240 {
241 if (n == 0) {
242 throw dimension_error("a mean over no element has no value in an integer type");
243 }
244 return value / static_cast<T>(n);
245 }
246
253 template <std::floating_point T,
254 typename Order>
256 const T* first,
257 std::size_t count,
258 std::size_t stride,
259 const Order& larger)
260 {
261 if (value != T {0}) {
262 return value;
263 }
264 const bool maximum {larger(T {1}, T {0})};
265 for (std::size_t i {0}; i < count; ++i) {
266 const T x {first[i * stride]};
267 if (x == T {0} && std::signbit(x) != maximum) {
268 return x;
269 }
270 }
271 return value;
272 }
273
275 template <typename T,
276 typename Order>
277 [[nodiscard]] T settled_zero(T value,
278 const T* /*first*/,
279 std::size_t /*count*/,
280 std::size_t /*stride*/,
281 const Order& /*larger*/)
282 {
283 return value;
284 }
285
287 [[nodiscard]] inline std::vector<std::size_t> without_axis(std::vector<std::size_t> shape,
288 std::size_t axis)
289 {
290 shape.erase(shape.begin() + static_cast<std::ptrdiff_t>(axis));
291 return shape;
292 }
293
298 template <typename T,
299 typename Reduce>
301 std::size_t axis,
302 const Reduce& reduce)
303 {
304 const axis_walk walk {walk_along(t.shape(), axis)};
305 buffer<T> out(walk.outer * walk.inner);
306 for (std::size_t o {0}; o < walk.outer; ++o) {
307 for (std::size_t i {0}; i < walk.inner; ++i) {
308 out[(o * walk.inner) + i] = reduce(t.data().data() + (o * walk.extent * walk.inner) + i, walk.extent, walk.inner);
309 }
310 }
311 return tensor<T> {without_axis(t.shape(), axis), std::move(out)};
312 }
313
315 template <typename T>
316 [[nodiscard]] T square(const T& x)
317 {
318 return static_cast<T>(x * x);
319 }
320
323 template <typename U>
324 [[nodiscard]] std::complex<U> square(const std::complex<U>& x)
325 {
326 return {(x.real() * x.real()) - (x.imag() * x.imag()), (x.real() * x.imag()) + (x.imag() * x.real())};
327 }
328
330 template <typename T>
331 [[nodiscard]] bool is_nan(const T& /*x*/)
332 {
333 return false;
334 }
335
337 [[nodiscard]] inline bool is_nan(double x)
338 {
339 return std::isnan(x);
340 }
341
342#if defined(__GNUC__)
344 template <typename V,
345 typename T>
346 [[nodiscard]] auto better(const V& x,
347 const V& best,
348 const std::greater<T>& /*larger*/)
349 {
350 return x > best;
351 }
352
354 template <typename V,
355 typename T>
356 [[nodiscard]] auto better(const V& x,
357 const V& best,
358 const std::less<T>& /*smaller*/)
359 {
360 return x < best;
361 }
362
364 template <typename V>
365 [[nodiscard]] auto unordered(const V& a,
366 const V& b)
367 {
368 return a != b;
369 }
370
371#endif
372
378 template <typename T,
379 typename Order>
380 [[nodiscard]] T contiguous_extreme(const T* first,
381 std::size_t count,
382 const Order& larger)
383 {
384 T result {first[0]};
385 bool nan {false};
386 std::size_t done {0};
387#if defined(__GNUC__)
388 using lane = typename native_vector<T>::type;
389 constexpr std::size_t width {native_vector_bytes / sizeof(T)};
390 constexpr std::size_t lanes {4}; // independent vectors, so no comparison waits on the one before it
391 if (count >= width * lanes) {
392 using mask = decltype(lane {} > lane {}); // all ones in a lane where a comparison holds
393 std::array<lane, lanes> best {};
394 std::array<mask, lanes> nans {};
395 for (std::size_t l {0}; l < lanes; ++l) {
396 std::memcpy(&best[l], first + (l * width), sizeof(lane));
397 }
398 done = count - (count % (width * lanes));
399 for (std::size_t i {0}; i < done; i += width * lanes) {
400# pragma GCC unroll 4 // the lanes in registers: GCC keeps a rolled array of vectors in memory
401 for (std::size_t l {0}; l < lanes; ++l) {
402 lane x;
403 std::memcpy(&x, first + i + (l * width), sizeof(lane));
404 // Blended through the comparison's all-ones / all-zeros mask: both compilers keep it in vector
405 // registers (GCC's `?:` on vectors does not).
406 const mask take {better(x, best[l], larger)};
407 best[l] = std::bit_cast<lane>((std::bit_cast<mask>(x) & take) | (std::bit_cast<mask>(best[l]) & ~take));
408 nans[l] |= unordered(x, x);
409 }
410 }
411 for (std::size_t l {0}; l < lanes; ++l) {
412 for (std::size_t k {0}; k < width; ++k) {
413 nan = nan || nans[l][k] != 0;
414 result = larger(best[l][k], result) ? best[l][k] : result;
415 }
416 }
417 }
418#endif
419 std::array<T, 8> best {};
420 std::array<unsigned, 8> nans {};
421 best.fill(result);
422 for (std::size_t i {done}; i < count; ++i) {
423 const T x {first[i]};
424 nans[i % 8] |= static_cast<unsigned>(std::isnan(x));
425 best[i % 8] = larger(x, best[i % 8]) ? x : best[i % 8];
426 }
427 for (std::size_t l {0}; l < 8; ++l) {
428 nan = nan || nans[l] != 0U;
429 result = larger(best[l], result) ? best[l] : result;
430 }
431 return nan ? std::numeric_limits<T>::quiet_NaN() : result;
432 }
433
435 template <typename T,
436 typename Order>
437 [[nodiscard]] T extreme(const T* first,
438 std::size_t count,
439 std::size_t stride,
440 const Order& larger)
441 {
442 if (count == 0) {
443 throw dimension_error("a zero-size reduction to a maximum or a minimum has no identity");
444 }
445 if constexpr (std::is_floating_point_v<T>) {
446 if (stride == 1) {
447 // A contiguous line: the comparison without a branch (so it vectorises) and the NaN tracked apart,
448 // a NaN anywhere is the answer, as NumPy's.
449 return settled_zero(contiguous_extreme(first, count, larger), first, count, 1, larger);
450 }
451 }
452 T best {first[0]};
453 for (std::size_t i {1}; i < count; ++i) {
454 // A NaN wins, as in NumPy: every comparison with it is false, so a NaN candidate is taken apart, and
455 // once taken nothing compares larger than it.
456 const T& candidate {first[i * stride]};
458 best = candidate;
459 }
460 }
461 return settled_zero(best, first, count, stride, larger);
462 }
463
469 template <typename T,
470 typename Order>
472 std::size_t axis,
473 const Order& larger)
474 {
475 const axis_walk walk {walk_along(t.shape(), axis)};
476 // An empty axis has no maximum even when the result is empty too (NumPy refuses both).
477 if (walk.extent == 0) {
478 throw dimension_error("a zero-size reduction to a maximum or a minimum has no identity");
479 }
480 if (!std::is_floating_point_v<T> || walk.inner == 1) {
481 return reduce_along(t, axis, [&larger](const T* first, std::size_t count, std::size_t stride) {
482 return extreme(first, count, stride, larger);
483 });
484 }
485 buffer<T> out(walk.outer * walk.inner);
486 std::vector<unsigned> nan(walk.inner);
487 for (std::size_t o {0}; o < walk.outer; ++o) {
488 const T* const slab {t.data().data() + (o * walk.extent * walk.inner)};
489 T* const best {out.data() + (o * walk.inner)};
490 std::copy(slab, slab + walk.inner, best);
491 std::fill(nan.begin(), nan.end(), 0U);
492 for (std::size_t e {0}; e < walk.extent; ++e) {
493 const T* const row {slab + (e * walk.inner)};
494 for (std::size_t i {0}; i < walk.inner; ++i) {
495 const T x {row[i]};
496 nan[i] |= static_cast<unsigned>(std::isnan(x));
497 best[i] = larger(x, best[i]) ? x : best[i];
498 }
499 }
500 for (std::size_t i {0}; i < walk.inner; ++i) {
501 best[i] = settled_zero(best[i], slab + i, walk.extent, walk.inner, larger);
502 if (nan[i] != 0U) {
503 best[i] = std::numeric_limits<T>::quiet_NaN();
504 }
505 }
506 }
507 return tensor<T> {without_axis(t.shape(), axis), std::move(out)};
508 }
509 } // namespace detail
510
515 [[nodiscard]] inline std::vector<std::size_t> broadcast_shape(const std::vector<std::size_t>& a,
516 const std::vector<std::size_t>& b)
517 {
518 const std::size_t rank {std::max(a.size(), b.size())};
519 std::vector<std::size_t> out(rank);
520 for (std::size_t i {0}; i < rank; ++i) {
521 const std::size_t x {i < rank - a.size() ? 1 : a[i - (rank - a.size())]};
522 const std::size_t y {i < rank - b.size() ? 1 : b[i - (rank - b.size())]};
523 if (x != y && x != 1 && y != 1) {
524 throw dimension_error("operands could not be broadcast together with shapes " + detail::shape_text(a) + " "
525 + detail::shape_text(b));
526 }
527 out[i] = x == 1 ? y : x;
528 }
529 return out;
530 }
531
536 template <typename T,
537 typename Op>
539 const tensor<T>& b,
540 const Op& op)
541 {
542 if (a.shape() == b.shape()) {
543 buffer<T> out(a.size());
544 const std::size_t chunks {(a.size() + detail::elementwise_chunk - 1) / detail::elementwise_chunk};
545 detail::parallel_for(chunks, detail::elementwise_chunk, [&](std::size_t c) {
546 const std::size_t first {c * detail::elementwise_chunk};
547 const std::size_t last {std::min(first + detail::elementwise_chunk, a.size())};
548 std::transform(a.data().begin() + static_cast<std::ptrdiff_t>(first), a.data().begin() + static_cast<std::ptrdiff_t>(last),
549 b.data().begin() + static_cast<std::ptrdiff_t>(first), out.begin() + static_cast<std::ptrdiff_t>(first), op);
550 });
551 return tensor<T> {a.shape(), std::move(out)};
552 }
553 std::vector<std::size_t> shape {broadcast_shape(a.shape(), b.shape())};
554 const std::size_t rank {shape.size()};
555 const std::vector<std::size_t> sa {detail::broadcast_strides(a.shape(), rank)};
556 const std::vector<std::size_t> sb {detail::broadcast_strides(b.shape(), rank)};
557 const std::size_t count {detail::element_count(shape.begin(), shape.end())};
558 buffer<T> out(count); // every element written below
559 if (count == 0) {
560 return tensor<T> {std::move(shape), std::move(out)};
561 }
562 // The innermost axis runs as one loop, each operand stepping 1 or 0 (a broadcast one) along it, so a
563 // scalar or a row against a matrix is a plain vector loop; the odometer turns only between rows. The rows go
564 // by tasks, each task's odometer set from its first row.
565 const std::size_t inner {shape[rank - 1]};
566 const std::size_t step_a {sa[rank - 1]};
567 const std::size_t step_b {sb[rank - 1]};
568 const std::size_t rows {count / inner};
569 const std::size_t rows_per_task {std::max<std::size_t>(1, detail::elementwise_chunk / inner)};
570 detail::parallel_for((rows + rows_per_task - 1) / rows_per_task, rows_per_task * inner, [&](std::size_t task) {
571 const std::size_t first {task * rows_per_task};
572 const std::size_t last {std::min(first + rows_per_task, rows)};
573 std::vector<std::size_t> index(rank, 0);
574 std::size_t ia {0};
575 std::size_t ib {0};
576 for (std::size_t axis {rank - 1}, rest {first}; axis-- > 0;) { // the first row's position
577 index[axis] = rest % shape[axis];
578 rest /= shape[axis];
579 ia += index[axis] * sa[axis];
580 ib += index[axis] * sb[axis];
581 }
582 for (std::size_t row {first}; row < last; ++row) {
583 const T* const pa {a.data().data() + ia};
584 const T* const pb {b.data().data() + ib};
585 T* const po {out.data() + (row * inner)};
586 if (step_a == 1 && step_b == 1) {
587 for (std::size_t i {0}; i < inner; ++i) {
588 po[i] = op(pa[i], pb[i]);
589 }
590 }
591 else if (step_a == 1) {
592 const T y {pb[0]};
593 for (std::size_t i {0}; i < inner; ++i) {
594 po[i] = op(pa[i], y);
595 }
596 }
597 else if (step_b == 1) {
598 const T x {pa[0]};
599 for (std::size_t i {0}; i < inner; ++i) {
600 po[i] = op(x, pb[i]);
601 }
602 }
603 else {
604 std::fill(po, po + inner, op(pa[0], pb[0]));
605 }
606 for (std::size_t axis {rank - 1}; axis-- > 0;) { // odometer over the outer axes: carry on wrap
607 ++index[axis];
608 ia += sa[axis];
609 ib += sb[axis];
610 if (index[axis] < shape[axis]) {
611 break;
612 }
613 ia -= sa[axis] * shape[axis];
614 ib -= sb[axis] * shape[axis];
615 index[axis] = 0;
616 }
617 }
618 });
619 return tensor<T> {std::move(shape), std::move(out)};
620 }
621
627 template <detail::split_complex C>
629 const tensor<C>& b)
630 {
632 return broadcast_apply(a, b, [fused](const C& x, const C& y) { return detail::numpy_multiply(x, y, fused); });
633 }
634
637 template <detail::split_complex C>
639 const tensor<C>& b)
640 {
641 const bool fused {detail::numpy_fuses_quotients()};
642 return broadcast_apply(a, b, [fused](const C& x, const C& y) { return detail::numpy_divide(x, y, fused); });
643 }
644
646 template <typename T>
648 const tensor<T>& b)
649 {
650 return broadcast_apply(a, b, std::plus<T> {});
651 }
652
654 template <typename T>
656 const tensor<T>& b)
657 {
658 return broadcast_apply(a, b, std::minus<T> {});
659 }
660
662 template <typename T>
664 const tensor<T>& b)
665 {
666 return broadcast_apply(a, b, std::multiplies<T> {});
667 }
668
670 template <typename T>
672 const tensor<T>& b)
673 {
674 return broadcast_apply(a, b, std::divides<T> {});
675 }
676
678 template <typename T>
680 {
681 buffer<T> out(a.size());
682 std::transform(a.data().begin(), a.data().end(), out.begin(), std::negate<T> {});
683 return tensor<T> {a.shape(), std::move(out)};
684 }
685
687 template <typename T,
688 typename F>
689 [[nodiscard]] auto map(const tensor<T>& t,
691 {
692 using R = std::decay_t<decltype(f(std::declval<const T&>()))>;
693 buffer<R> out(t.size());
694 std::transform(t.data().begin(), t.data().end(), out.begin(), f);
695 return tensor<R> {t.shape(), std::move(out)};
696 }
697
703 template <typename T>
705 const tensor<T>& b)
706 {
707 if constexpr (!std::is_integral_v<T>) {
708 if (b.size() == 1 && b.shape().size() <= a.shape().size()) {
709 const T y {b.data()[0]};
710 if (y == T(2)) {
711 return map(a, [](const T& x) { return detail::square(x); });
712 }
713 if (y == T(0.5)) {
714 return map(a, [](const T& x) { return static_cast<T>(std::sqrt(x)); });
715 }
716 if (y == T(-1)) {
717 return map(a, [](const T& x) { return static_cast<T>(T(1) / x); });
718 }
719 }
720 }
721 return broadcast_apply(a, b, [](const T& x, const T& y) { return static_cast<T>(std::pow(x, y)); });
722 }
723
725 template <typename T>
726 [[nodiscard]] T sum(const tensor<T>& t)
727 {
728 return detail::pairwise_sum(t.data().data(), t.size(), 1);
729 }
730
732 template <typename T>
734 std::size_t axis)
735 {
737 buffer<T> out(walk.outer * walk.inner);
738 detail::pairwise_sums(t.data().data(), walk.outer, walk.extent, walk.inner, out.data());
739 return tensor<T> {detail::without_axis(t.shape(), axis), std::move(out)};
740 }
741
745 template <typename T>
746 [[nodiscard]] T mean(const tensor<T>& t)
747 {
748 return detail::divided_by_count(sum(t), t.size());
749 }
750
752 template <typename T>
754 std::size_t axis)
755 {
757 buffer<T> out(walk.outer * walk.inner);
758 detail::pairwise_sums(t.data().data(), walk.outer, walk.extent, walk.inner, out.data());
759 for (T& x : out) {
760 x = detail::divided_by_count(x, walk.extent);
761 }
762 return tensor<T> {detail::without_axis(t.shape(), axis), std::move(out)};
763 }
764
766 template <std::totally_ordered T>
767 [[nodiscard]] T max(const tensor<T>& t)
768 {
769 return detail::extreme(t.data().data(), t.size(), 1, std::greater<T> {});
770 }
771
773 template <std::totally_ordered T>
774 [[nodiscard]] T min(const tensor<T>& t)
775 {
776 return detail::extreme(t.data().data(), t.size(), 1, std::less<T> {});
777 }
778
780 template <std::totally_ordered T>
782 std::size_t axis)
783 {
784 return detail::extremes_along(t, axis, std::greater<T> {});
785 }
786
788 template <std::totally_ordered T>
790 std::size_t axis)
791 {
792 return detail::extremes_along(t, axis, std::less<T> {});
793 }
794
799 template <typename T>
801 std::vector<std::size_t> shape)
802 {
803 std::size_t count {1};
804 for (const std::size_t extent : shape) {
805 count = detail::checked_mul(count, extent);
806 }
807 if (count != t.size()) {
808 throw dimension_error("cannot reshape a tensor of size " + std::to_string(t.size()) + " into shape "
809 + detail::shape_text(shape));
810 }
811 return tensor<T> {std::move(shape), t.data()};
812 }
813
815 template <typename T>
816 [[nodiscard]] tensor<T> full(std::vector<std::size_t> shape,
817 const T& value)
818 {
819 std::size_t count {1};
820 for (const std::size_t extent : shape) {
821 count = detail::checked_mul(count, extent);
822 }
823 return tensor<T> {std::move(shape), buffer<T>(count, value)};
824 }
825
827 template <typename T>
828 [[nodiscard]] tensor<T> zeros(std::vector<std::size_t> shape)
829 {
830 return full<T>(std::move(shape), T {});
831 }
832
834 template <typename T>
835 [[nodiscard]] tensor<T> ones(std::vector<std::size_t> shape)
836 {
837 return full<T>(std::move(shape), static_cast<T>(1));
838 }
839
841 template <typename T>
842 [[nodiscard]] tensor<T> eye(std::size_t n)
843 {
845 for (std::size_t i {0}; i < n; ++i) {
846 out[(i * n) + i] = static_cast<T>(1);
847 }
848 return tensor<T> {{n, n}, std::move(out)};
849 }
850
859 [[nodiscard]] inline tensor<double> arange(double start,
860 double stop,
861 double step)
862 {
863 if (step == 0.0 || !std::isfinite(step) || !std::isfinite(start) || !std::isfinite(stop)) {
864 throw dimension_error("arange needs finite bounds and a finite, non-zero step");
865 }
866 const double length {std::ceil((stop - start) / step)};
867 if (!(length < 0x1p64)) { // past std::size_t, or an infinite span: no conversion holds it
868 throw dimension_error("arange: the length does not fit in std::size_t");
869 }
870 const std::size_t count {length > 0.0 ? static_cast<std::size_t>(length) : 0};
871 buffer<double> out(count);
872 const double delta {(start + step) - start};
873 for (std::size_t i {0}; i < count; ++i) {
874 out[i] = std::fma(static_cast<double>(i), delta, start);
875 }
876 return tensor<double> {{count}, std::move(out)};
877 }
878
884 [[nodiscard]] inline tensor<double> linspace(double start,
885 double stop,
886 std::size_t count)
887 {
888 buffer<double> out(count);
889 const double step {count > 1 ? (stop - start) / static_cast<double>(count - 1) : 0.0};
890 for (std::size_t i {0}; i < count; ++i) {
891 // The product rounded before the sum, as NumPy's linspace computes it in two operations. A compiler
892 // may fuse `i * step` into the addition after it (GCC does, across statements, wherever there is a
893 // fused multiply-add); a product computed by fma, `i * step - 0`, is exactly the rounded one, signed
894 // zero included, and no multiplication is left to fuse.
895 const double offset {std::fma(static_cast<double>(i), step, -0.0)};
896 out[i] = start + offset;
897 }
898 if (count > 1) {
899 out[count - 1] = stop;
900 }
901 return tensor<double> {{count}, std::move(out)};
902 }
903} // namespace numerical
904
905#endif // NUMERICAL_TENSOR_OPS_HPP
Thrown when an operation's operands have incompatible shapes.
Definition vector.hpp:35
A dense, row-major, N-dimensional tensor of T with value semantics.
Definition tensor.hpp:63
const std::vector< std::size_t > & shape() const noexcept
The size of each axis.
Definition tensor.hpp:136
std::size_t size() const noexcept
The total number of elements.
Definition tensor.hpp:130
const buffer< T > & data() const noexcept
The elements, row-major.
Definition tensor.hpp:142
bool is_nan(const T &)
A NaN? Never for a scalar without one.
T divided_by_count(T value, std::size_t n)
A mean: value divided by the count n, in the wider of T and double and back to T, the count exact (to...
T pairwise_sum(const T *first, std::size_t count, std::size_t stride)
Sum of count values from first at stride stride, by NumPy's pairwise summation: below 8 values in ord...
Definition tensor.hpp:216
axis_walk walk_along(const std::vector< std::size_t > &shape, std::size_t axis)
axis_walk of shape along axis; outer and inner 0 where the result is empty.
bool numpy_fuses_scalar_products() noexcept
Whether NumPy's complex product fuses its multiply-adds in its scalar loop (where numpy_loop_is_scala...
T square(const T &x)
x · x.
T settled_zero(T value, const T *first, std::size_t count, std::size_t stride, const Order &larger)
The extreme value of count elements stride apart from first, its zero's sign settled as IEEE 754-2019...
bool numpy_loop_is_scalar(const tensor< T > &a, const tensor< T > &b) noexcept
Whether NumPy multiplies a by b in its scalar loop rather than its vector one: when the result is one...
bool numpy_fuses_products() noexcept
Whether NumPy's complex product fuses its multiply-adds on this processor: on arm64,...
std::string shape_text(const std::vector< std::size_t > &shape)
A shape as NumPy prints it, (2, 3), for a message.
C numpy_multiply(const C &a, const C &b, bool fused)
a · b of complex numbers as NumPy's array loop computes it: ai·bi and ai·br rounded,...
constexpr std::size_t elementwise_chunk
The elements one task of an element-wise operation takes, a quarter of a thread's work: a tensor of a...
Definition parallel.hpp:539
bool numpy_fuses_quotients() noexcept
Whether NumPy's complex quotient fuses its multiply-adds, as its PyPI wheels are built: on macOS arm6...
T contiguous_extreme(const T *first, std::size_t count, const Order &larger)
The largest (larger is std::greater) or smallest of count contiguous elements, or a NaN if one is amo...
T extreme(const T *first, std::size_t count, std::size_t stride, const Order &larger)
The largest (larger is std::greater) or smallest element of a line.
void parallel_for(std::size_t count, std::size_t work_per_index, const Body &body)
Runs body(i) for every i in [0, count).
Definition parallel.hpp:580
double real_part(const T &x)
The real part of a real (or general) scalar as a double.
std::size_t checked_mul(std::size_t a, std::size_t b)
a * b, throwing numerical::dimension_error if the product overflows std::size_t. Guards dimension/sha...
Definition vector.hpp:50
std::vector< std::size_t > without_axis(std::vector< std::size_t > shape, std::size_t axis)
The shape with axis axis removed.
C numpy_divide(const C &a, const C &b, bool fused)
a / b of complex numbers as NumPy's array loop computes it: Smith's division by the larger part of b ...
void pairwise_sums(const T *data, std::size_t outer, std::size_t count, std::size_t inner, T *out)
The pairwise sums of outer blocks of count rows of inner elements (row-major), each column of a block...
Definition tensor.hpp:322
std::vector< std::size_t > broadcast_strides(const std::vector< std::size_t > &shape, std::size_t rank)
The row-major strides of shape, with a stride of 0 on each axis of extent 1 (it broadcasts),...
std::size_t element_count(It first, It last)
The elements a shape's extents [first, last) hold: 0 where one of them is 0, whatever the others,...
Definition vector.hpp:66
tensor< T > reduce_along(const tensor< T > &t, std::size_t axis, const Reduce &reduce)
Reduces each line of t along axis by reduce, which takes the line's first element,...
tensor< T > extremes_along(const tensor< T > &t, std::size_t axis, const Order &larger)
The largest (larger is std::greater) or smallest elements along axis. Along an inner axis each line i...
@ fused
std::fma, where every target has the instruction.
R numpy_muladd(R x, R y, R z, bool fused)
x · y + z: one fused multiply-add where fused, else the product rounded first (statements apart,...
The NumericAL public API.
Definition amx.hpp:44
std::vector< T, detail::default_init_allocator< T > > buffer
The storage of vector, matrix and tensor, namely a std::vector whose buffer<T>(n) and resize(n) leave...
Definition buffer.hpp:157
tensor< T > broadcast_apply(const tensor< T > &a, const tensor< T > &b, const Op &op)
op(a, b) element by element, the operands broadcast against each other.
T sum(const tensor< T > &t)
The sum of every element, by pairwise halving (0 for an empty tensor).
matrix< T > operator+(const matrix< T > &a, const matrix< T > &b)
Element-wise sum.
Definition matrix.hpp:161
tensor< double > linspace(double start, double stop, std::size_t count)
count values evenly spaced from start to stop, both included, as NumPy's linspace: element i is start...
tensor< C > numpy_divide(const tensor< C > &a, const tensor< C > &b)
a / b of complex tensors, broadcast, each quotient as NumPy's array loop computes it (detail::numpy_d...
matrix< T > operator*(const T &scalar, const matrix< T > &m)
Scalar times matrix.
Definition matrix.hpp:187
tensor< T > operator/(const tensor< T > &a, const tensor< T > &b)
Element-wise quotient, broadcast.
tensor< C > numpy_multiply(const tensor< C > &a, const tensor< C > &b)
a · b of complex tensors, broadcast, each product as NumPy's array loop computes it (detail::numpy_mu...
tensor< T > ones(std::vector< std::size_t > shape)
A tensor of shape full of ones.
std::vector< std::size_t > broadcast_shape(const std::vector< std::size_t > &a, const std::vector< std::size_t > &b)
The shape two shapes broadcast to, as NumPy's broadcast_shapes.
@ amx
The Apple AMX coprocessor, fused multiply-adds; the products it does not suit on the NEON path's tile...
T max(const tensor< T > &t)
The largest element (a NaN, if there is one).
tensor< T > zeros(std::vector< std::size_t > shape)
A tensor of shape full of zeros.
matrix< T > operator-(const matrix< T > &a, const matrix< T > &b)
Element-wise difference.
Definition matrix.hpp:174
auto map(const tensor< T > &t, const F &f) -> tensor< std::decay_t< decltype(f(std::declval< const T & >()))> >
f(x) for every element, in a tensor of the same shape and of f's result type.
tensor< T > eye(std::size_t n)
The n × n identity.
tensor< double > arange(double start, double stop, double step)
start, start + step, … below stop (above it for a negative step), as NumPy's arange: its length is ce...
tensor< T > reshape(const tensor< T > &t, std::vector< std::size_t > shape)
The same elements in the same order under another shape.
T mean(const tensor< T > &t)
The mean of every element. An empty tensor's mean is NaN, as NumPy's (which also warns).
T min(const tensor< T > &t)
The smallest element (a NaN, if there is one).
tensor< T > full(std::vector< std::size_t > shape, const T &value)
A tensor of shape with every element value.
tensor< T > pow(const tensor< T > &a, const tensor< T > &b)
a to the power b element by element, broadcast, as std::pow; a single exponent of 2,...
The native vector of float and double the kernels hold their lanes in (GNU vector extensions,...
The extents and strides that walk a tensor of shape with axis axis taken out: outer positions before ...
std::size_t outer
The product of the extents before the axis.
std::size_t inner
The product of the extents after the axis.
std::size_t extent
The axis' extent.
A dense, row-major, N-dimensional tensor and Einstein summation.