Skip to content

Commit befe03a

Browse files
Use relative errors by default when comparing output files (#449)
1 parent 0b4ab46 commit befe03a

7 files changed

Lines changed: 235 additions & 64 deletions

File tree

GridKit/Testing/AggregateErrors.hpp

Lines changed: 166 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include <cmath>
44
#include <format>
5+
#include <limits>
6+
#include <memory>
57
#include <vector>
68

79
#include <GridKit/Testing/OutputAtTime.hpp>
@@ -11,96 +13,228 @@ namespace GridKit
1113
namespace Testing
1214
{
1315

16+
inline constexpr double DEFAULT_ABS_ERROR_THRESHOLD =
17+
std::numeric_limits<double>::epsilon();
18+
1419
/**
15-
* @brief Aggregate error data for a single variable
20+
* @brief Aggregate norm data for a single variable over time
1621
*/
17-
struct ErrorAggregate
22+
struct TemporalNormAggregate
1823
{
24+
/// Name of variable
1925
std::string label;
20-
double max_error{0.0};
21-
double max_error_time{0.0};
22-
double error_norm_L2{0.0};
26+
/// L-inf norm
27+
double max_value{0.0};
28+
/// Time at which max value occurred
29+
double max_value_time{0.0};
30+
/// L2 norm
31+
double L2{0.0};
2332

24-
void push(double err, double time)
33+
/**
34+
* @brief Add a new value for the variable to be aggregated
35+
*/
36+
void push(double val, double time)
2537
{
26-
if (err > max_error)
38+
if (val > max_value)
2739
{
28-
max_error = err;
29-
max_error_time = time;
40+
max_value = val;
41+
max_value_time = time;
3042
}
31-
error_norm_L2 += err * err;
43+
L2 += val * val;
3244
}
3345

46+
/**
47+
* @brief Finalize the calculation(s)
48+
*/
3449
void wrap()
3550
{
36-
error_norm_L2 = std::sqrt(error_norm_L2);
51+
L2 = std::sqrt(L2);
52+
}
53+
54+
/**
55+
* @brief Scale by a reference value (only if the current value is above
56+
* the given threshold (used internally for relative errors)
57+
*/
58+
void scale(const TemporalNormAggregate& ref, double threshold)
59+
{
60+
if (ref.max_value > threshold)
61+
{
62+
max_value /= ref.max_value;
63+
}
64+
if (ref.L2 > threshold)
65+
{
66+
L2 /= ref.L2;
67+
}
3768
}
3869

70+
/**
71+
* @brief Pretty-print label and values to output stream
72+
*/
3973
std::ostream& display(
4074
std::ostream& os = std::cout, const std::string& indent = " ") const
4175
{
4276
os << indent << label << ":\n"
4377
<< indent << indent << "max : "
44-
<< std::format("{:.6e} (at time {:.3e})", max_error, max_error_time)
78+
<< std::format("{:.6e} (at time {:.3e})", max_value, max_value_time)
4579
<< '\n'
4680
<< indent << indent << "L2-norm : "
47-
<< std::format("{:.6e}", error_norm_L2) << '\n';
81+
<< std::format("{:.6e}", L2) << '\n';
4882
return os;
4983
}
5084
};
5185

5286
/**
53-
* @brief A set of ErrorAggregate for each variable plus a total (combined)
54-
* ErrorAggregate
87+
* @brief A set of aggregate norms (represented with TemporalNormAggregate)
88+
* for the error in each variable plus one for the total (combined) error
5589
*
56-
* @note The "total" aggregate is based on the L-infinity norm of the local
57-
* error of variables at a given time step.
90+
* @note The "total_error" aggregate is based on the L-infinity norm of the
91+
* local error of variables at a given time step.
5892
*/
5993
struct ErrorSet
6094
{
61-
ErrorAggregate total{"Total"};
62-
std::vector<ErrorAggregate> vars{};
95+
/// Aggregate of the combined error value at each time step
96+
TemporalNormAggregate total_error{"Total"};
97+
/// Aggregate error for each variable
98+
std::vector<TemporalNormAggregate> var_errors{};
6399

100+
/**
101+
* @brief Construct with variable labels
102+
*/
64103
template <typename C>
65104
explicit ErrorSet(const C& labels)
66-
: vars(std::size(labels))
105+
: var_errors(std::size(labels))
67106
{
68107
for (std::size_t i = 0; i < std::size(labels); ++i)
69108
{
70-
vars[i].label = labels[i];
109+
var_errors[i].label = labels[i];
71110
}
72111
}
73112

74-
void push(const OutputAtTime& err)
113+
virtual ~ErrorSet()
75114
{
76-
for (std::size_t i = 0; i < vars.size(); ++i)
77-
{
78-
vars[i].push(std::abs(err.data[i]), err.t);
79-
}
80-
total.push(lInfNorm(err), err.t);
81115
}
82116

83-
void wrap()
117+
/**
118+
* @brief Finalize the calculations
119+
*/
120+
virtual void wrap()
84121
{
85-
for (auto& agg : vars)
122+
for (auto& agg : var_errors)
86123
{
87124
agg.wrap();
88125
}
89-
total.wrap();
126+
total_error.wrap();
90127
}
91128

129+
/**
130+
* @brief Take the error between the two output parameters for each
131+
* variable and add to the aggregate
132+
*/
133+
virtual void push(const OutputAtTime&, const OutputAtTime&) = 0;
134+
135+
/**
136+
* @brief Pretty-print the set of errors for each variable and total
137+
*/
92138
std::ostream& display(std::ostream& os = std::cout) const
93139
{
94140
std::string indent{" "};
95141
os << "Error Set:\n";
96-
for (const auto& var : vars)
142+
for (const auto& var : var_errors)
97143
{
98144
var.display(os, indent);
99145
}
100-
total.display(os, indent);
146+
total_error.display(os, indent);
101147
return os;
102148
}
103149
};
104150

151+
enum class ErrorType
152+
{
153+
RELATIVE,
154+
ABSOLUTE
155+
};
156+
157+
struct RelativeError;
158+
struct AbsoluteError;
159+
160+
template <typename error_type = RelativeError>
161+
struct ErrorSetImpl;
162+
163+
template <>
164+
struct ErrorSetImpl<RelativeError> : ErrorSet
165+
{
166+
template <typename C>
167+
explicit ErrorSetImpl(
168+
const C& labels,
169+
double abs_threshold = DEFAULT_ABS_ERROR_THRESHOLD)
170+
: ErrorSet(labels),
171+
abs_threshold_(abs_threshold),
172+
ref_norms_(std::size(labels))
173+
{
174+
}
175+
176+
void push(const OutputAtTime& test, const OutputAtTime& ref) override
177+
{
178+
auto err = test - ref;
179+
for (std::size_t i = 0; i < var_errors.size(); ++i)
180+
{
181+
var_errors[i].push(std::abs(err.data[i]), err.t);
182+
ref_norms_[i].push(std::abs(ref.data[i]), ref.t);
183+
}
184+
total_error.push(lInfNorm(err), err.t);
185+
ref_total_norm_.push(lInfNorm(ref), ref.t);
186+
}
187+
188+
void wrap() override
189+
{
190+
ErrorSet::wrap();
191+
for (std::size_t i = 0; i < var_errors.size(); ++i)
192+
{
193+
ref_norms_[i].wrap();
194+
var_errors[i].scale(ref_norms_[i], abs_threshold_);
195+
}
196+
ref_total_norm_.wrap();
197+
total_error.scale(ref_total_norm_, abs_threshold_);
198+
}
199+
200+
private:
201+
double abs_threshold_{};
202+
TemporalNormAggregate ref_total_norm_{};
203+
std::vector<TemporalNormAggregate> ref_norms_{};
204+
};
205+
206+
template <>
207+
struct ErrorSetImpl<AbsoluteError> : ErrorSet
208+
{
209+
using ErrorSet::ErrorSet;
210+
211+
void push(const OutputAtTime& test, const OutputAtTime& ref) override
212+
{
213+
auto err = test - ref;
214+
for (std::size_t i = 0; i < var_errors.size(); ++i)
215+
{
216+
var_errors[i].push(std::abs(err.data[i]), err.t);
217+
}
218+
total_error.push(lInfNorm(err), err.t);
219+
}
220+
};
221+
222+
template <typename C>
223+
std::unique_ptr<ErrorSet> makeErrorSet(
224+
ErrorType type,
225+
const C& labels,
226+
double abs_threshold = DEFAULT_ABS_ERROR_THRESHOLD)
227+
{
228+
switch (type)
229+
{
230+
case ErrorType::RELATIVE:
231+
return std::make_unique<ErrorSetImpl<RelativeError>>(labels, abs_threshold);
232+
case ErrorType::ABSOLUTE:
233+
return std::make_unique<ErrorSetImpl<AbsoluteError>>(labels);
234+
default:
235+
throw std::runtime_error("Invalid error type");
236+
}
237+
}
238+
105239
} // namespace Testing
106240
} // namespace GridKit

GridKit/Testing/CSV.cpp

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,50 +35,54 @@ namespace GridKit
3535
return ifs;
3636
}
3737

38-
ErrorSet compareCSV(const std::string& f_a, const std::string& f_b)
38+
std::unique_ptr<ErrorSet> compareCSV(const std::string& f_test,
39+
const std::string& f_ref,
40+
ErrorType error_type,
41+
double abs_threshold)
3942
{
4043
// Open files and read labels
41-
auto ifs_a = checkOpenFile(f_a);
42-
auto ifs_b = checkOpenFile(f_b);
44+
auto ifs_test = checkOpenFile(f_test);
45+
auto ifs_ref = checkOpenFile(f_ref);
4346

44-
std::string line_a;
45-
std::getline(ifs_a, line_a);
46-
auto labels_a = Tokenizer<>(line_a, ',')();
47+
std::string line_test;
48+
std::getline(ifs_test, line_test);
49+
auto labels_a = Tokenizer<>(line_test, ',')();
4750

48-
std::string line_b;
49-
std::getline(ifs_b, line_b);
50-
auto labels_b = Tokenizer<>(line_b, ',')();
51+
std::string line_ref;
52+
std::getline(ifs_ref, line_ref);
53+
auto labels_b = Tokenizer<>(line_ref, ',')();
5154

5255
if (labels_a.size() != labels_b.size())
5356
{
54-
Log::error() << "Files \"" << f_a << "\" and \"" << f_b
57+
Log::error() << "Files \"" << f_test << "\" and \"" << f_ref
5558
<< "\" have different number of variables." << std::endl;
5659
}
5760

5861
// Create error set
59-
auto err = ErrorSet(std::span{next(begin(labels_a)), end(labels_a)});
62+
auto var_labels = std::span{next(begin(labels_a)), end(labels_a)};
63+
auto err = makeErrorSet(error_type, var_labels, abs_threshold);
6064

61-
while (ifs_a && ifs_b)
65+
while (ifs_test && ifs_ref)
6266
{
63-
std::getline(ifs_a, line_a);
64-
std::getline(ifs_b, line_b);
65-
if (!(ifs_a && ifs_b))
67+
std::getline(ifs_test, line_test);
68+
std::getline(ifs_ref, line_ref);
69+
if (!(ifs_test && ifs_ref))
6670
{
67-
if (ifs_a || ifs_b)
71+
if (ifs_test || ifs_ref)
6872
{
69-
Log::error() << "Files \"" << f_a << "\" and \"" << f_b
73+
Log::error() << "Files \"" << f_test << "\" and \"" << f_ref
7074
<< "\" have different lengths." << std::endl;
7175
}
7276
break;
7377
}
7478

75-
OutputAtTime d_a(Tokenizer<double>(line_b, ',')());
76-
OutputAtTime d_b(Tokenizer<double>(line_a, ',')());
79+
OutputAtTime d_test(Tokenizer<double>(line_test, ',')());
80+
OutputAtTime d_ref(Tokenizer<double>(line_ref, ',')());
7781

78-
err.push(d_a - d_b);
82+
err->push(d_test, d_ref);
7983
}
8084

81-
err.wrap();
85+
err->wrap();
8286

8387
return err;
8488
}

GridKit/Testing/CSV.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ namespace GridKit
2020
*
2121
* @note This assumes a "time" variable is always in the first column.
2222
*/
23-
ErrorSet compareCSV(const std::string& f_a, const std::string& f_b);
23+
std::unique_ptr<ErrorSet> compareCSV(
24+
const std::string& test_file,
25+
const std::string& reference_file,
26+
ErrorType error_type = ErrorType::RELATIVE,
27+
double abs_threshold = DEFAULT_ABS_ERROR_THRESHOLD);
2428

2529
} // namespace Testing
2630
} // namespace GridKit

0 commit comments

Comments
 (0)