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 0 : 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 0 : format_config get_default_format_config()
47 : {
48 0 : format_config cfg;
49 : if constexpr (isComplex<data_t>) {
50 : cfg.summary_items /= 2;
51 : }
52 :
53 0 : return cfg;
54 : }
55 :
56 : /// stream a pretty format to is using the current configuration.
57 0 : void format(std::ostream& os, const DataContainer<data_t>& dc)
58 : {
59 : using namespace std::string_literals;
60 :
61 0 : auto dims = dc.getDataDescriptor().getNumberOfDimensions();
62 0 : auto shape = dc.getDataDescriptor().getNumberOfCoefficientsPerDimension();
63 0 : os << "DataContainer<dims=" << dims << ", shape=(" << shape.transpose() << ")>"
64 0 : << std::endl;
65 :
66 0 : auto format_element = this->get_element_formatter<data_t>(dc);
67 :
68 : std::function<void(std::ostream & stream, index_t current_dim, IndexVector_t & index,
69 : std::string_view indent_prefix)>
70 0 : recurser;
71 0 : recurser = [this, &recurser, &dc, &dims, &shape,
72 : &format_element](std::ostream& os, index_t current_dim,
73 : IndexVector_t& index, std::string_view hanging_indent) {
74 0 : index_t dims_left = dims - current_dim;
75 0 : index_t next_dim = current_dim + 1;
76 :
77 0 : if (dims_left == 0) {
78 : // get the actual element, recursion terminator! \o/
79 0 : format_element(os, dc(index));
80 0 : return;
81 : }
82 :
83 0 : std::string next_hanging_indent = std::string(hanging_indent) + " "s;
84 :
85 0 : index_t dim_size = shape(current_dim);
86 0 : auto backidx = [dim_size](index_t idx) { return dim_size - idx; };
87 :
88 : index_t leading_items, trailing_items;
89 0 : bool do_summary =
90 0 : (this->config.summary_enabled and 2 * this->config.summary_items < dim_size);
91 0 : if (do_summary) {
92 0 : leading_items = this->config.summary_items;
93 0 : trailing_items = this->config.summary_items;
94 : } else {
95 0 : leading_items = 0;
96 0 : trailing_items = dim_size;
97 : }
98 :
99 0 : os << "[";
100 :
101 : // last dimension, i.e. the rows
102 0 : if (dims_left == 1) {
103 :
104 : // TODO c++20 use views::iota
105 0 : for (index_t i = 0; i < leading_items; i++) {
106 0 : index(current_dim) = i;
107 0 : recurser(os, next_dim, index, next_hanging_indent);
108 0 : os << this->config.separator;
109 : }
110 :
111 0 : if (do_summary) {
112 0 : os << this->config.summary_elem << this->config.separator;
113 : }
114 :
115 0 : for (index_t i = trailing_items; i > 1; i--) {
116 0 : index(current_dim) = backidx(i);
117 0 : recurser(os, next_dim, index, next_hanging_indent);
118 0 : os << this->config.separator;
119 : }
120 :
121 0 : index(current_dim) = backidx(1);
122 0 : recurser(os, next_dim, index, next_hanging_indent);
123 : } else {
124 : // newlines between rows
125 : // the more dimensions, the more newlines.
126 0 : auto line_separator = (std::string(this->rstrip(this->config.separator))
127 0 : + std::string(dims_left - 1, '\n'));
128 :
129 0 : 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 : }
134 0 : recurser(os, next_dim, index, next_hanging_indent);
135 0 : os << line_separator;
136 : }
137 :
138 0 : if (do_summary) {
139 0 : os << hanging_indent << " " << this->config.summary_elem_vertical
140 0 : << line_separator;
141 : }
142 :
143 : // remaining but the last element
144 0 : for (index_t i = trailing_items; i > 1; i--) {
145 0 : index(current_dim) = backidx(i);
146 0 : if (do_summary or i != trailing_items) {
147 0 : os << hanging_indent;
148 : }
149 0 : recurser(os, next_dim, index, next_hanging_indent);
150 0 : os << line_separator;
151 : }
152 :
153 : // indent for the current dim's last entry
154 : // we skip it if it's the only entry
155 0 : if (trailing_items > 1) {
156 0 : os << hanging_indent;
157 : }
158 :
159 : // print the last element
160 0 : index(current_dim) = backidx(1);
161 0 : recurser(os, next_dim, index, next_hanging_indent);
162 0 : }
163 :
164 0 : os << "]";
165 0 : };
166 :
167 : // we'll modify this index on the fly when we walk over the datacontainer
168 0 : IndexVector_t dc_index{dims};
169 0 : auto prefix = " "s; // skip over initial [
170 0 : recurser(os, 0, dc_index, prefix);
171 0 : }
172 :
173 : private:
174 : /**
175 : * adjust a given string_view and remove all whitespace from the right side.
176 : */
177 0 : std::string_view rstrip(std::string_view text)
178 : {
179 0 : auto end_it = std::rbegin(text);
180 0 : while (end_it != std::rend(text) and *end_it == ' ') {
181 0 : ++end_it;
182 : }
183 :
184 : // TODO c++20: use stringview iterator constructor directly
185 0 : const char* start = &(*std::begin(text));
186 0 : const char* end = &(*end_it) + 1;
187 :
188 0 : ssize_t len = end - start;
189 0 : if (len < 0) {
190 0 : throw elsa::InternalError{"rstrip length sanity check failed"};
191 : }
192 0 : return std::string_view(start, static_cast<size_t>(len));
193 : }
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 0 : get_element_formatter(const DataContainer<T>& dc)
209 : {
210 : if constexpr (elsa::isComplex<T>) {
211 : // format both components independently
212 0 : auto real_formatter = get_element_formatter(DataContainer<T>{real(dc)});
213 0 : auto imag_formatter = get_element_formatter(DataContainer<T>{imag(dc)});
214 :
215 0 : return [real_formatter, imag_formatter](std::ostream & os, const T& elem) -> auto&
216 : {
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 0 : } 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 0 : if (val_max > 1e7
234 0 : or (not suppress_small and (val_min < 0.0001 or val_max / val_min > 1000.0))) {
235 :
236 0 : use_exp = true;
237 : }
238 :
239 : // TODO: possible optimization - no underlying string needed,
240 : // could be a /dev/null-like storage,
241 : // since we only use tellp.
242 0 : std::ostringstream teststream;
243 0 : if (use_exp) {
244 0 : teststream << std::scientific;
245 : } else {
246 0 : teststream << std::defaultfloat;
247 : }
248 :
249 : // setw wants int...
250 0 : int maxlen = 0;
251 0 : for (elsa::index_t idx = 0; idx < dc.getSize(); ++idx) {
252 0 : teststream.str("");
253 0 : teststream.clear();
254 :
255 0 : auto&& elem = config.suppress_close_to_zero
256 0 : && std::abs(dc[idx]) < config.suppression_epsilon
257 0 : ? static_cast<data_t>(0)
258 0 : : dc[idx];
259 :
260 0 : teststream << elem;
261 0 : auto len = static_cast<int>(teststream.tellp());
262 :
263 0 : if (len > maxlen) {
264 0 : maxlen = len;
265 : }
266 : }
267 :
268 0 : auto streamflags = teststream.flags();
269 :
270 0 : return [
271 0 : maxlen, streamflags, do_suppress = config.suppress_close_to_zero,
272 0 : eps = config.suppression_epsilon
273 : ](std::ostream & os, const T& elem) -> auto&
274 : {
275 0 : os.flags(streamflags);
276 0 : os << std::setw(maxlen);
277 0 : os << (do_suppress && std::abs(elem) < eps ? static_cast<data_t>(0) : elem);
278 0 : return os;
279 0 : };
280 0 : } else {
281 : // setw wants int, string::size returns size_t. great.
282 0 : int maxlen = static_cast<int>(std::max(std::to_string(dc.maxElement()).size(),
283 0 : std::to_string(dc.minElement()).size()));
284 0 : return [maxlen](std::ostream & os, const T& elem) -> auto&
285 : {
286 0 : auto&& elem_str = std::to_string(elem);
287 0 : os << std::setw(maxlen);
288 0 : os << std::to_string(elem);
289 0 : return os;
290 0 : };
291 : }
292 : }
293 :
294 : /// formatting output configuration
295 : format_config config;
296 : };
297 :
298 : } // namespace elsa
|