Line data Source code
1 : #pragma once
2 :
3 : #include "elsaDefines.h"
4 : #include "DataContainer.h"
5 : #include "FormatConfig.h"
6 :
7 : #include <iomanip>
8 : #include <iostream>
9 : #include <iterator>
10 : #include <sstream>
11 : #include <string>
12 : #include <string_view>
13 : #include <type_traits>
14 :
15 : namespace elsa
16 : {
17 :
18 : /**
19 : * @brief Pretty printing for elsa::DataContainer.
20 : *
21 : * Inspired by numpy's formatter, supports n-dimensional string representation
22 : * of stored values in a `DataContainer`.
23 : *
24 : * @author Jonas Jelten - initial code
25 : *
26 : * @tparam data_t: datacontainer element type
27 : */
28 : template <typename data_t>
29 : class DataContainerFormatter
30 : {
31 : public:
32 : /**
33 : * Create a formatter with default config.
34 : */
35 3 : DataContainerFormatter() : config{this->get_default_format_config()} {}
36 :
37 : /**
38 : * Create a formatter with custom config.
39 : */
40 0 : DataContainerFormatter(const format_config& config) : config{config} {}
41 :
42 : /**
43 : * get the default formatting config.
44 : * the class defaults are adjusted due to the `data_t`.
45 : */
46 : format_config get_default_format_config()
47 3 : {
48 3 : format_config cfg;
49 3 : if constexpr (isComplex<data_t>) {
50 3 : cfg.summary_items /= 2;
51 3 : }
52 :
53 3 : return cfg;
54 3 : }
55 :
56 : /// stream a pretty format to is using the current configuration.
57 : void format(std::ostream& os, const DataContainer<data_t>& dc)
58 3 : {
59 3 : using namespace std::string_literals;
60 :
61 3 : auto dims = dc.getDataDescriptor().getNumberOfDimensions();
62 3 : auto shape = dc.getDataDescriptor().getNumberOfCoefficientsPerDimension();
63 3 : os << "DataContainer<dims=" << dims << ", shape=(" << shape.transpose() << ")>"
64 3 : << std::endl;
65 :
66 3 : auto format_element = this->get_element_formatter<data_t>(dc);
67 :
68 3 : std::function<void(std::ostream & stream, index_t current_dim, IndexVector_t & index,
69 3 : std::string_view indent_prefix)>
70 3 : recurser;
71 3 : recurser = [this, &recurser, &dc, &dims, &shape,
72 3 : &format_element](std::ostream& os, index_t current_dim,
73 45 : IndexVector_t& index, std::string_view hanging_indent) {
74 45 : index_t dims_left = dims - current_dim;
75 45 : index_t next_dim = current_dim + 1;
76 :
77 45 : if (dims_left == 0) {
78 : // get the actual element, recursion terminator! \o/
79 37 : format_element(os, dc(index));
80 37 : return;
81 37 : }
82 :
83 8 : std::string next_hanging_indent = std::string(hanging_indent) + " "s;
84 :
85 8 : index_t dim_size = shape(current_dim);
86 36 : auto backidx = [dim_size](index_t idx) { return dim_size - idx; };
87 :
88 8 : index_t leading_items, trailing_items;
89 8 : bool do_summary =
90 8 : (this->config.summary_enabled and 2 * this->config.summary_items < dim_size);
91 8 : if (do_summary) {
92 1 : leading_items = this->config.summary_items;
93 1 : trailing_items = this->config.summary_items;
94 7 : } else {
95 7 : leading_items = 0;
96 7 : trailing_items = dim_size;
97 7 : }
98 :
99 8 : os << "[";
100 :
101 : // last dimension, i.e. the rows
102 8 : if (dims_left == 1) {
103 :
104 : // TODO c++20 use views::iota
105 13 : for (index_t i = 0; i < leading_items; i++) {
106 6 : index(current_dim) = i;
107 6 : recurser(os, next_dim, index, next_hanging_indent);
108 6 : os << this->config.separator;
109 6 : }
110 :
111 7 : if (do_summary) {
112 1 : os << this->config.summary_elem << this->config.separator;
113 1 : }
114 :
115 31 : for (index_t i = trailing_items; i > 1; i--) {
116 24 : index(current_dim) = backidx(i);
117 24 : recurser(os, next_dim, index, next_hanging_indent);
118 24 : os << this->config.separator;
119 24 : }
120 :
121 7 : index(current_dim) = backidx(1);
122 7 : recurser(os, next_dim, index, next_hanging_indent);
123 7 : } else {
124 : // newlines between rows
125 : // the more dimensions, the more newlines.
126 1 : auto line_separator = (std::string(this->rstrip(this->config.separator))
127 1 : + std::string(static_cast<size_t>(dims_left) - 1, '\n'));
128 :
129 1 : for (index_t i = 0; i < leading_items; i++) {
130 0 : index(current_dim) = i;
131 0 : if (i != 0) {
132 0 : os << hanging_indent;
133 0 : }
134 0 : recurser(os, next_dim, index, next_hanging_indent);
135 0 : os << line_separator;
136 0 : }
137 :
138 1 : if (do_summary) {
139 0 : os << hanging_indent << " " << this->config.summary_elem_vertical
140 0 : << line_separator;
141 0 : }
142 :
143 : // remaining but the last element
144 5 : for (index_t i = trailing_items; i > 1; i--) {
145 4 : index(current_dim) = backidx(i);
146 4 : if (do_summary or i != trailing_items) {
147 3 : os << hanging_indent;
148 3 : }
149 4 : recurser(os, next_dim, index, next_hanging_indent);
150 4 : os << line_separator;
151 4 : }
152 :
153 : // indent for the current dim's last entry
154 : // we skip it if it's the only entry
155 1 : if (trailing_items > 1) {
156 1 : os << hanging_indent;
157 1 : }
158 :
159 : // print the last element
160 1 : index(current_dim) = backidx(1);
161 1 : recurser(os, next_dim, index, next_hanging_indent);
162 1 : }
163 :
164 8 : os << "]";
165 8 : };
166 :
167 : // we'll modify this index on the fly when we walk over the datacontainer
168 3 : IndexVector_t dc_index{dims};
169 3 : auto prefix = " "s; // skip over initial [
170 3 : recurser(os, 0, dc_index, prefix);
171 3 : }
172 :
173 : private:
174 : /**
175 : * adjust a given string_view and remove all whitespace from the right side.
176 : */
177 : std::string_view rstrip(std::string_view text)
178 1 : {
179 1 : auto end_it = std::rbegin(text);
180 2 : while (end_it != std::rend(text) and *end_it == ' ') {
181 1 : ++end_it;
182 1 : }
183 :
184 : // TODO c++20: use stringview iterator constructor directly
185 1 : const char* start = &(*std::begin(text));
186 1 : const char* end = &(*end_it) + 1;
187 :
188 1 : ssize_t len = end - start;
189 1 : if (len < 0) {
190 0 : throw elsa::InternalError{"rstrip length sanity check failed"};
191 0 : }
192 1 : return std::string_view(start, static_cast<size_t>(len));
193 1 : }
194 :
195 : /**
196 : * Given a DataContainer, generate a function that will format one element beautifully.
197 : * This element is padded to the max width of the other elements.
198 : *
199 : * @param dc: data to get the formatter for.
200 : *
201 : * TODO add new features:
202 : * unique: display elements in such a way they are uniquely distinguishable
203 : * (dynamically adjust precision)
204 : * precision: if unique is false, use this float precision.
205 : */
206 : template <typename T>
207 : std::function<std::ostream&(std::ostream& os, const T& elem)>
208 : get_element_formatter(const DataContainer<T>& dc)
209 3 : {
210 3 : if constexpr (elsa::isComplex<T>) {
211 : // format both components independently
212 3 : auto real_formatter = get_element_formatter(real(dc));
213 3 : auto imag_formatter = get_element_formatter(imag(dc));
214 :
215 3 : return [real_formatter, imag_formatter](std::ostream & os, const T& elem) -> auto&
216 3 : {
217 0 : real_formatter(os, elem.real());
218 0 : os << "+";
219 0 : imag_formatter(os, elem.imag());
220 0 : os << "j";
221 0 : return os;
222 0 : };
223 3 : } else if constexpr (std::is_floating_point_v<T>) {
224 : // TODO: handle non-finite (not nan, inf) elements
225 : // TODO: align stuff at the . and do right and left padding
226 :
227 0 : bool suppress_small = true;
228 0 : bool use_exp = false;
229 :
230 0 : T val_max = dc.maxElement();
231 0 : T val_min = dc.minElement();
232 :
233 3 : if (val_max > 1e7f
234 3 : or (not suppress_small
235 3 : and (val_min < 0.0001f or val_max / val_min > 1000.0f))) {
236 :
237 0 : use_exp = true;
238 0 : }
239 :
240 : // TODO: possible optimization - no underlying string needed,
241 : // could be a /dev/null-like storage,
242 : // since we only use tellp.
243 0 : std::ostringstream teststream;
244 3 : if (use_exp) {
245 0 : teststream << std::scientific;
246 3 : } else {
247 3 : teststream << std::defaultfloat;
248 3 : }
249 :
250 : // setw wants int...
251 0 : int maxlen = 0;
252 43 : for (elsa::index_t idx = 0; idx < dc.getSize(); ++idx) {
253 40 : teststream.str("");
254 40 : teststream.clear();
255 :
256 40 : auto&& elem = config.suppress_close_to_zero
257 40 : && std::abs(dc[idx]) < config.suppression_epsilon
258 40 : ? static_cast<data_t>(0)
259 40 : : dc[idx];
260 :
261 40 : teststream << elem;
262 40 : auto len = static_cast<int>(teststream.tellp());
263 :
264 40 : if (len > maxlen) {
265 3 : maxlen = len;
266 3 : }
267 40 : }
268 :
269 0 : auto streamflags = teststream.flags();
270 :
271 0 : return [
272 0 : maxlen, streamflags, do_suppress = config.suppress_close_to_zero,
273 0 : eps = config.suppression_epsilon
274 0 : ](std::ostream & os, const T& elem) -> auto&
275 37 : {
276 37 : os.flags(streamflags);
277 37 : os << std::setw(maxlen);
278 37 : os << (do_suppress && std::abs(elem) < eps ? static_cast<data_t>(0) : elem);
279 37 : return os;
280 37 : };
281 3 : } else {
282 : // setw wants int, string::size returns size_t. great.
283 0 : int maxlen = static_cast<int>(std::max(std::to_string(dc.maxElement()).size(),
284 0 : std::to_string(dc.minElement()).size()));
285 0 : return [maxlen](std::ostream & os, const T& elem) -> auto&
286 0 : {
287 0 : auto&& elem_str = std::to_string(elem);
288 0 : os << std::setw(maxlen);
289 0 : os << std::to_string(elem);
290 0 : return os;
291 0 : };
292 0 : }
293 3 : }
294 :
295 : /// formatting output configuration
296 : format_config config;
297 : };
298 :
299 : } // namespace elsa
|