UFO
test/ufo/ObsFunction.h
Go to the documentation of this file.
1 /*
2  * (C) Copyright 2019 UCAR
3  *
4  * This software is licensed under the terms of the Apache Licence Version 2.0
5  * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
6  */
7 
8 #ifndef TEST_UFO_OBSFUNCTION_H_
9 #define TEST_UFO_OBSFUNCTION_H_
10 
11 #include <iomanip>
12 #include <memory>
13 #include <string>
14 #include <vector>
15 
16 #define ECKIT_TESTING_SELF_REGISTER_CASES 0
17 
18 #include "eckit/config/LocalConfiguration.h"
19 #include "eckit/testing/Test.h"
20 #include "ioda/distribution/DistributionUtils.h"
21 #include "ioda/ObsSpace.h"
22 #include "ioda/ObsVector.h"
23 #include "oops/mpi/mpi.h"
24 #include "oops/runs/Test.h"
25 #include "oops/util/Expect.h"
26 #include "oops/util/missingValues.h"
27 #include "test/interface/ObsTestsFixture.h"
28 #include "test/TestEnvironment.h"
31 #include "ufo/filters/Variables.h"
32 #include "ufo/GeoVaLs.h"
33 #include "ufo/ObsDiagnostics.h"
34 #include "ufo/ObsTraits.h"
35 
36 namespace eckit
37 {
38  // Don't use the contracted output for these types: the current implementation works only
39  // with integer types.
40  template <> struct VectorPrintSelector<util::DateTime> { typedef VectorPrintSimple selector; };
41 } // namespace eckit
42 
43 // -----------------------------------------------------------------------------
44 
45 namespace ufo {
46 namespace test {
47 
48 // -----------------------------------------------------------------------------
49 
50 const char *expectComputeToThrow = "expect compute to throw exception with message";
51 const char *expectConstructorToThrow = "expect constructor to throw exception with message";
52 
53 // -----------------------------------------------------------------------------
54 
55 /// \param[out] num_missing_mismatches
56 /// Number of locations containing a missing value in `vals` but not in `ref`, or in `ref` but not
57 /// in `vals`.
58 void dataVectorDiff(const ioda::ObsSpace & ospace, ioda::ObsDataVector<float> & vals,
59  const ioda::ObsDataVector<float> & ref, std::vector<float> & rms_out,
60  size_t &num_missing_mismatches) {
61  const float missing = util::missingValue(missing);
62  num_missing_mismatches = 0;
63  /// Loop through variables and calculate rms for each variable
64  for (size_t ivar = 0; ivar < vals.nvars() ; ++ivar) {
65  for (size_t jj = 0; jj < ref.nlocs() ; ++jj) {
66  if (vals[ivar][jj] != missing && ref[ivar][jj] != missing) {
67  vals[ivar][jj] -= ref[ivar][jj];
68  } else {
69  vals[ivar][jj] = missing;
70  }
71  if ((vals[ivar][jj] != missing) ^ (ref[ivar][jj] != missing)) {
72  ++num_missing_mismatches;
73  }
74  }
75  int nobs = globalNumNonMissingObs(*ospace.distribution(), 1, vals[ivar]);
76  float rms = dotProduct(*ospace.distribution(), 1, vals[ivar], vals[ivar]);
77  if (nobs > 0) rms = sqrt(rms / static_cast<float>(nobs));
78  rms_out[ivar] = rms;
79  }
80 }
81 
82 // -----------------------------------------------------------------------------
83 
84 /// Checks that `vals` and `vals_ofd` are equal to `ref`.
85 ///
86 /// \param vals
87 /// ObsFunction values calculated by calling ObsFunction::compute() directly.
88 /// \param vals_ofd
89 /// ObsFunction values calculated by calling ObsFilterData::get().
90 /// \param ref
91 /// Reference values.
92 template <typename T>
93 void checkResults(const ioda::ObsSpace &/*ospace*/,
94  const eckit::Configuration &/*obsfuncconf*/,
95  const ioda::ObsDataVector<T> & vals,
96  const ioda::ObsDataVector<T> & vals_ofd,
97  const ioda::ObsDataVector<T> & ref) {
98  const size_t nvars = ref.nvars();
99  EXPECT_EQUAL(vals.nvars(), nvars);
100 
101  for (size_t i = 0; i < nvars; ++i)
102  EXPECT_EQUAL(vals[i], ref[i]);
103 
104  for (size_t i = 0; i < nvars; ++i)
105  EXPECT_EQUAL(vals_ofd[i], ref[i]);
106 }
107 
108 /// Specialization for floats: expects the RMSs of the differences between `vals` and `ref` and
109 /// between `vals_ofd` and `ref` to be within a specified tolerance.
110 ///
111 /// The contents of `vals` and `vals_ofd` are destroyed.
112 void checkResults(const ioda::ObsSpace &ospace,
113  const eckit::Configuration &obsfuncconf,
115  ioda::ObsDataVector<float> & vals_ofd,
116  const ioda::ObsDataVector<float> & ref) {
117  const double tol = obsfuncconf.getDouble("tolerance");
118  const bool expectMissingToMatch =
119  obsfuncconf.getBool("expect missing value locations to match", false);
120  const size_t nvars = ref.nvars();
121  EXPECT_EQUAL(vals.nvars(), nvars);
122 
123  /// Calculate rms(f(x) - ref) and compare to tolerance
124  std::vector<float> rms_out(nvars);
125  size_t numMissingMismatches;
126  dataVectorDiff(ospace, vals, ref, rms_out, numMissingMismatches);
127 
128  oops::Log::info() << "Vector difference between reference and computed: " << std::endl;
129  oops::Log::info() << vals << std::endl;
130  for (size_t ivar = 0; ivar < nvars; ivar++) {
131  // FIXME(someone): whatever this does, it certainly doesn't convert percentages to fractions
132  EXPECT(rms_out[ivar] < 100*tol); // change tol from percent to actual value.
133  }
134  if (expectMissingToMatch)
135  EXPECT_EQUAL(numMissingMismatches, 0);
136 
137  dataVectorDiff(ospace, vals_ofd, ref, rms_out, numMissingMismatches);
138  oops::Log::info() << "Vector difference between reference and computed via ObsFilterData: "
139  << std::endl;
140  oops::Log::info() << vals_ofd << std::endl;
141  for (size_t ivar = 0; ivar < nvars; ivar++) {
142  // FIXME(someone): whatever this does, it certainly doesn't convert percentages to fractions
143  EXPECT(rms_out[ivar] < 100*tol); // change tol from percent to actual value.
144  }
145  if (expectMissingToMatch)
146  EXPECT_EQUAL(numMissingMismatches, 0);
147 }
148 
149 // -----------------------------------------------------------------------------
150 
151 template <typename T>
152 void doTestFunction(ioda::ObsSpace &ospace, const eckit::Configuration &conf) {
153 /// Get function name
154  const eckit::LocalConfiguration obsfuncconf(conf, "obs function");
155  Variable funcname(obsfuncconf);
156 
157 /// Setup function
158  if (conf.has(expectConstructorToThrow)) {
159  // The constructor is expected to throw an exception containing the specified string.
160  const std::string expectedMessage = conf.getString(expectConstructorToThrow);
161  EXPECT_THROWS_MSG(ObsFunction<T>{funcname}, expectedMessage.c_str());
162  return;
163  }
164  ObsFunction<T> obsfunc(funcname);
165  ufo::Variables allfuncvars = obsfunc.requiredVariables();
166 
167 /// Setup ObsFilterData
168  ObsFilterData inputs(ospace);
169 
170 /// Setup GeoVaLs
171  const oops::Variables geovars = allfuncvars.allFromGroup("GeoVaLs").toOopsVariables();
172  std::unique_ptr<GeoVaLs> gval;
173  if (geovars.size() > 0) {
174  const eckit::LocalConfiguration gconf(conf, "geovals");
175  gval.reset(new GeoVaLs(gconf, ospace, geovars));
176  inputs.associate(*gval);
177  }
178 
179 /// Setup zero ObsBias
180  ioda::ObsVector bias(ospace);
181  bias.zero();
182  inputs.associate(bias, "ObsBiasData");
183 
184 /// Setup ObsDiags
185  const oops::Variables diagvars = allfuncvars.allFromGroup("ObsDiag").toOopsVariables();
186  std::unique_ptr<ObsDiagnostics> diags;
187  if (diagvars.size() > 0) {
188  const eckit::LocalConfiguration diagconf(conf, "obs diagnostics");
189  diags.reset(new ObsDiagnostics(diagconf, ospace, diagvars));
190  inputs.associate(*diags);
191  }
192 
193 /// Get output variable names
194  const oops::Variables outputvars(obsfuncconf, "variables");
195 /// Compute function result
196  ioda::ObsDataVector<T> vals(ospace, outputvars, funcname.group(), false);
197  if (!conf.has(expectComputeToThrow)) {
198  obsfunc.compute(inputs, vals);
199  } else {
200  // The call to compute() is expected to throw an exception containing the specified string.
201  const std::string expectedMessage = conf.getString(expectComputeToThrow);
202  EXPECT_THROWS_MSG(obsfunc.compute(inputs, vals), expectedMessage.c_str());
203  return;
204  }
205  vals.save("TestResult");
206 
207 /// Compute function result through ObsFilterData
208  ioda::ObsDataVector<T> vals_ofd(ospace, outputvars, funcname.group(), false);
209  inputs.get(funcname, vals_ofd);
210 
211 /// Read reference values from ObsSpace
212  ioda::ObsDataVector<T> ref(ospace, outputvars, "TestReference");
213 
214  checkResults(ospace, obsfuncconf, vals, vals_ofd, ref);
215 }
216 
217 // -----------------------------------------------------------------------------
218 
219 void testFunction() {
220  typedef ::test::ObsTestsFixture<ObsTraits> Test_;
221 
222  std::vector<eckit::LocalConfiguration> typeconfs;
223  ::test::TestEnvironment::config().get("observations", typeconfs);
224 
225  for (std::size_t jj = 0; jj < Test_::obspace().size(); ++jj) {
226  ioda::ObsSpace &ospace = Test_::obspace()[jj].obsspace();
227  const eckit::Configuration &conf = typeconfs[jj];
228 
229 /// Use the function group to determine the type of values produced by the function and thus
230 /// select the right specialization of doTestFunction().
231  Variable funcname(conf.getSubConfiguration("obs function"));
232  if (funcname.group() == ObsFunctionTraits<float>::groupName)
233  doTestFunction<float>(ospace, conf);
234  else if (funcname.group() == ObsFunctionTraits<int>::groupName)
235  doTestFunction<int>(ospace, conf);
236  else if (funcname.group() == ObsFunctionTraits<std::string>::groupName)
237  doTestFunction<std::string>(ospace, conf);
239  doTestFunction<util::DateTime>(ospace, conf);
240  else
241  throw eckit::testing::TestException("Variable " + funcname.fullName() +
242  " is not an ObsFunction", Here());
243  }
244 }
245 
246 // -----------------------------------------------------------------------------
247 
248 class ObsFunction : public oops::Test {
249  public:
251  virtual ~ObsFunction() {}
252  private:
253  std::string testid() const override {return "ufo::test::ObsFunction";}
254 
255  void register_tests() const override {
256  std::vector<eckit::testing::Test>& ts = eckit::testing::specification();
257 
258  ts.emplace_back(CASE("ufo/ObsFunction/testFunction")
259  { testFunction(); });
260  }
261 
262  void clear() const override {
263  ::test::ObsTestsFixture<ObsTraits>::reset();
264  }
265 };
266 
267 // -----------------------------------------------------------------------------
268 
269 } // namespace test
270 } // namespace ufo
271 
272 #endif // TEST_UFO_OBSFUNCTION_H_
std::string fullName() const
Definition: Variable.cc:128
const std::string & group() const
Definition: Variable.cc:116
Variables allFromGroup(const std::string &) const
Definition: Variables.cc:128
void register_tests() const override
void clear() const override
std::string testid() const override
Forward declarations.
Definition: ObsAodExt.h:21
const char * expectComputeToThrow
void dataVectorDiff(const ioda::ObsSpace &ospace, ioda::ObsDataVector< float > &vals, const ioda::ObsDataVector< float > &ref, std::vector< float > &rms_out, size_t &num_missing_mismatches)
void checkResults(const ioda::ObsSpace &, const eckit::Configuration &, const ioda::ObsDataVector< T > &vals, const ioda::ObsDataVector< T > &vals_ofd, const ioda::ObsDataVector< T > &ref)
CASE("ufo/DataExtractor/bilinearinterp/float_linear")
const char * expectConstructorToThrow
void doTestFunction(ioda::ObsSpace &ospace, const eckit::Configuration &conf)
Definition: RunCRTM.h:27
Common properties of ObsFunctions producing values of type FunctionValue.