UFO
test/ufo/ConventionalProfileProcessing.h
Go to the documentation of this file.
1 /*
2  * (C) Crown copyright 2020, Met Office
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_CONVENTIONALPROFILEPROCESSING_H_
9 #define TEST_UFO_CONVENTIONALPROFILEPROCESSING_H_
10 
11 #include <iomanip>
12 #include <memory>
13 #include <set>
14 #include <string>
15 #include <utility>
16 #include <vector>
17 
18 #define ECKIT_TESTING_SELF_REGISTER_CASES 0
19 
20 #include "eckit/config/LocalConfiguration.h"
21 #include "eckit/testing/Test.h"
22 #include "ioda/ObsDataVector.h"
23 #include "ioda/ObsSpace.h"
24 #include "ioda/ObsVector.h"
25 #include "oops/mpi/mpi.h"
26 #include "oops/runs/Test.h"
27 #include "oops/util/Expect.h"
28 #include "oops/util/FloatCompare.h"
29 #include "test/TestEnvironment.h"
33 #include "ufo/filters/Variables.h"
34 #include "ufo/GeoVaLs.h"
35 #include "ufo/ObsDiagnostics.h"
36 
46 
53 
55 
56 namespace ufo {
57 namespace test {
58 
59 void testConventionalProfileProcessing(const eckit::LocalConfiguration &conf) {
60  util::DateTime bgn(conf.getString("window begin"));
61  util::DateTime end(conf.getString("window end"));
62 
63  const eckit::LocalConfiguration obsSpaceConf(conf, "obs space");
64  ioda::ObsTopLevelParameters obsParams;
65  obsParams.validateAndDeserialize(obsSpaceConf);
66  ioda::ObsSpace obsspace(obsParams, oops::mpi::world(), bgn, end, oops::mpi::myself());
67 
68  const Variables filtervars = Variables(obsspace.obsvariables());
69 
70  ObsFilterData filterdata(obsspace);
71 
72  ioda::ObsVector hofx(obsspace);
73 
74  ioda::ObsVector bias(obsspace);
75  bias.zero();
76 
77  const eckit::LocalConfiguration obsdiagconf(conf, "obs diagnostics");
78  std::vector<eckit::LocalConfiguration> varconfs;
79  obsdiagconf.get("variables", varconfs);
80  const Variables diagvars(varconfs);
81  const ObsDiagnostics obsdiags(obsdiagconf, obsspace, diagvars.toOopsVariables());
82 
83  std::shared_ptr<ioda::ObsDataVector<float>> obserr(new ioda::ObsDataVector<float>(
84  obsspace, obsspace.obsvariables(), "ObsError"));
85 
86  std::shared_ptr<ioda::ObsDataVector<int>> qcflags(new ioda::ObsDataVector<int>(
87  obsspace, obsspace.obsvariables()));
88 
89  const eckit::LocalConfiguration filterConf(conf, "Conventional Profile Processing");
91  filterParameters.validateAndDeserialize(filterConf);
92 
93  // Determine whether an exception is expected to be thrown.
94  // Exceptions can be thrown in the following places:
95  // - on instantiation of the filter,
96  // - during the operation of the filter,
97  bool expectThrowOnInstantiation = conf.getBool("ExpectThrowOnInstantiation", false);
98  bool expectThrowDuringOperation = conf.getBool("ExpectThrowDuringOperation", false);
99 
100  if (expectThrowOnInstantiation) {
101  EXPECT_THROWS(ufo::ConventionalProfileProcessing filterThrow(obsspace, filterParameters,
102  qcflags, obserr));
103  // Do not proceed further in this case.
104  return;
105  }
106 
107  // Instantiate filter.
108  ufo::ConventionalProfileProcessing filter(obsspace, filterParameters, qcflags, obserr);
109 
110  // Obtain GeoVaLs.
111  const bool ignoreGeoVaLs = conf.getBool("IgnoreGeoVaLs", false);
112  const auto geovars = filter.requiredVars();
113  std::unique_ptr <GeoVaLs> geovals;
114  if (!ignoreGeoVaLs && geovars.size() > 0) {
115  const eckit::LocalConfiguration geovalsConf(conf, "geovals");
116  geovals.reset(new GeoVaLs(geovalsConf, obsspace, geovars));
117  } else {
118  geovals.reset(new GeoVaLs(obsspace.distribution(), oops::Variables()));
119  }
120 
121  filter.preProcess();
122  filter.priorFilter(*geovals);
123  if (expectThrowDuringOperation)
124  EXPECT_THROWS(filter.postFilter(hofx, bias, obsdiags));
125  else
126  filter.postFilter(hofx, bias, obsdiags);
127 
128  // Determine whether the mismatch check should be bypassed or not.
129  // It might be necessary to disable the mismatch check in tests which are
130  // designed to reach code paths that would normally result in failure.
131  const bool bypassMismatchComparison = conf.getBool("BypassMismatchComparison", false);
132 
133  // Check there are no mismatches between the values produced by this code and the OPS equivalents
134  if (!bypassMismatchComparison) {
135  for (auto nMM : filter.getMismatches())
136  EXPECT_EQUAL(nMM, 0);
137  }
138 
139  // === Additional tests of exceptions === //
140 
141  // Test whether adding the same check twice throws an exception.
142  const bool addDuplicateCheck = conf.getBool("AddDuplicateCheck", false);
143  if (addDuplicateCheck) {
144  static ProfileCheckMaker<ProfileCheckUInterp> makerDuplicate1_("duplicate");
145  EXPECT_THROWS(static ProfileCheckMaker<ProfileCheckUInterp> makerDuplicate2_("duplicate"));
146  }
147 
148  // Test whether using the get function with the wrong type throws an exception.
149  const bool getWrongType = conf.getBool("GetWrongType", false);
150  if (getWrongType) {
152  options.deserialize(conf);
153  EntireSampleDataHandler entireSampleDataHandler(obsspace,
154  options.DHParameters);
155  // Load data from obsspace
156  entireSampleDataHandler.get<float>(ufo::VariableNames::obs_air_pressure);
157  // Attempt to access data with incorrect type
158  EXPECT_THROWS(entireSampleDataHandler.get<int>(ufo::VariableNames::obs_air_pressure));
159  std::vector<bool> apply(obsspace.nlocs(), true);
160  std::vector<std::vector<bool>> flagged;
161  ProfileDataHandler profileDataHandler(filterdata,
162  options.DHParameters,
163  apply,
164  filtervars,
165  flagged);
166  // Obtain profile data
167  profileDataHandler.get<float>(ufo::VariableNames::obs_air_pressure);
168  // Attempt to access data with incorrect type
169  EXPECT_THROWS(profileDataHandler.get<int>(ufo::VariableNames::obs_air_pressure));
170  }
171 
172  // Manually modify Processing flags in order to cover rare code paths.
173  const bool ManualFlagModification = conf.getBool("ManualFlagModification", false);
174  if (ManualFlagModification) {
176  options.deserialize(conf);
177  EntireSampleDataHandler entireSampleDataHandler(obsspace,
178  options.DHParameters);
179  // Load data from obsspace
180  entireSampleDataHandler.get<float>(ufo::VariableNames::obs_air_pressure);
181  entireSampleDataHandler.get<int>(ufo::VariableNames::qcflags_air_temperature);
182  std::vector<bool> apply(obsspace.nlocs(), true);
183  std::vector<std::vector<bool>> flagged;
184  ProfileDataHandler profileDataHandler(filterdata,
185  options.DHParameters,
186  apply,
187  filtervars,
188  flagged);
189  ProfileCheckValidator profileCheckValidator(options);
190 
191  profileDataHandler.initialiseNextProfile();
192 
193  // Obtain profile data
194  profileDataHandler.get<float>(ufo::VariableNames::obs_air_pressure);
195 
196  // Modify flags
197  std::vector <int> &ReportFlags =
198  profileDataHandler.get<int>(ufo::VariableNames::qcflags_observation_report);
199  std::vector <int> &tFlags =
200  profileDataHandler.get<int>(ufo::VariableNames::qcflags_air_temperature);
201  std::vector <int> &rhFlags =
202  profileDataHandler.get<int>(ufo::VariableNames::qcflags_relative_humidity);
203  std::vector <int> &uFlags =
204  profileDataHandler.get<int>(ufo::VariableNames::qcflags_eastward_wind);
205  std::vector <int> &zFlags =
206  profileDataHandler.get<int>(ufo::VariableNames::qcflags_geopotential_height);
207  std::vector <int> &timeFlags =
208  profileDataHandler.get<int>(ufo::VariableNames::qcflags_time);
209 
218 
219  // Create checks
220  ProfileCheckTime profileCheckTime(options);
221  ProfileCheckBackgroundTemperature profileCheckBackgroundT(options);
222  ProfileCheckBackgroundRelativeHumidity profileCheckBackgroundRH(options);
223  ProfileCheckBackgroundWindSpeed profileCheckBackgroundUV(options);
224  ProfileCheckBackgroundGeopotentialHeight profileCheckBackgroundZ(options);
225 
226  // Run time check
227  profileCheckTime.runCheck(profileDataHandler);
228 
229  // Modify time flag
230  timeFlags[0] = true;
231 
232  // Run remaining checks
233  profileCheckBackgroundT.runCheck(profileDataHandler);
234  profileCheckBackgroundRH.runCheck(profileDataHandler);
235  profileCheckBackgroundUV.runCheck(profileDataHandler);
236  profileCheckBackgroundZ.runCheck(profileDataHandler);
237  }
238 
239  // Test the profile vertical interpolation
240  const bool testProfileVerticalInterpolation =
241  conf.getBool("testProfileVerticalInterpolation", false);
242  if (testProfileVerticalInterpolation) {
244  options.deserialize(conf);
245 
246  std::vector<bool> apply(obsspace.nlocs(), true);
247  std::vector<std::vector<bool>> flagged;
248  ProfileDataHandler profileDataHandler(filterdata,
249  options.DHParameters,
250  apply,
251  filtervars,
252  flagged);
253 
254  // Get interpolation options for each profile.
255  const auto interpMethodNames = conf.getStringVector("interpMethodNames");
256  const auto coordOrderNames = conf.getStringVector("coordOrderNames");
257  const auto outOfBoundsNames = conf.getStringVector("outOfBoundsNames");
258 
259  for (size_t jprof = 0; jprof < obsspace.nrecs(); ++jprof) {
260  profileDataHandler.initialiseNextProfile();
261 
262  const std::string interpMethodName = interpMethodNames[jprof];
263  const std::string coordOrderName = coordOrderNames[jprof];
264  const std::string outOfBoundsName = outOfBoundsNames[jprof];
265 
266  // Calculate level heights for GeoVaLs.
267  std::vector <float> orogGeoVaLs(obsspace.nlocs(), 0.0);
268  geovals->getAtLevel(orogGeoVaLs, ufo::VariableNames::geovals_orog, 0);
269  std::vector <float> zRhoGeoVaLs;
270  std::vector <float> zThetaGeoVaLs;
272  orogGeoVaLs[0],
273  zRhoGeoVaLs,
274  zThetaGeoVaLs);
275 
276  // Reverse coordinate order if required.
277  if (coordOrderName == "Descending")
278  std::reverse(zRhoGeoVaLs.begin(), zRhoGeoVaLs.end());
279 
280  // Create column of pressure GeoVaLs.
281  std::vector <float> pressureGeoVaLs(obsspace.nlocs(), 0.0);
282  const size_t gvnlevs = geovals->nlevs(ufo::VariableNames::geovals_pressure);
283  std::vector <float> pressureGeoVaLs_column;
284  for (int jlev = 0; jlev < gvnlevs; ++jlev) {
285  geovals->getAtLevel(pressureGeoVaLs, ufo::VariableNames::geovals_pressure, jlev);
286  pressureGeoVaLs_column.push_back(pressureGeoVaLs[0]);
287  }
288 
289  // Get observed geopotential height and (empty) pressure vector.
290  const auto &zObs = profileDataHandler.get<float>(ufo::VariableNames::obs_geopotential_height);
291  auto &pressures = profileDataHandler.get<float>(ufo::VariableNames::obs_air_pressure);
292 
294  if (interpMethodName == "LogLinear")
297  if (coordOrderName == "Descending")
300  if (outOfBoundsName == "SetMissing")
302  if (outOfBoundsName == "Extrapolate")
304 
305  // Interpolate to determine pressure.
306  profileVerticalInterpolation(zRhoGeoVaLs,
307  pressureGeoVaLs_column,
308  zObs,
309  pressures,
310  interpMethod,
311  coordOrder,
312  outOfBounds);
313 
314  // Compare each value of pressure.
315  const std::string expectedPressureName = "OPS_" +
316  static_cast<std::string>(ufo::VariableNames::obs_air_pressure);
317  const auto &expected_pressures = profileDataHandler.get<float>(expectedPressureName);
318  for (size_t jlev = 0; jlev < pressures.size(); ++jlev)
319  EXPECT(oops::is_close_relative(pressures[jlev], expected_pressures[jlev], 1e-5f));
320  }
321  }
322 
323  // Test the profile vertical averaging.
324  const bool testProfileVerticalAveraging =
325  conf.getBool("testProfileVerticalAveraging", false);
326  if (testProfileVerticalAveraging) {
328  options.deserialize(conf);
329 
330  std::vector<bool> apply(obsspace.nlocs(), true);
331  std::vector<std::vector<bool>> flagged;
332  ProfileDataHandler profileDataHandler(filterdata,
333  options.DHParameters,
334  apply,
335  filtervars,
336  flagged);
337 
338  for (size_t jprof = 0; jprof < obsspace.nrecs(); ++jprof) {
339  profileDataHandler.initialiseNextProfile();
340 
341  const auto &flagsIn = profileDataHandler.get<int>(ufo::VariableNames::qcflags_eastward_wind);
342  const auto &valuesIn = profileDataHandler.get<float>(ufo::VariableNames::obs_eastward_wind);
343  const auto &coordIn = profileDataHandler.get<float>(ufo::VariableNames::LogP_derived);
344  const auto &bigGap = profileDataHandler.get<float>(ufo::VariableNames::bigPgaps_derived);
345  const auto &coordOut = profileDataHandler.get<float>
347  const float DZFrac = 0.5;
348  const ProfileAveraging::Method method =
350 
351  std::vector <int> flagsOut;
352  std::vector <float> valuesOut;
353  int numGaps = 0;
354  std::vector<float> ZMax;
355  std::vector<float> ZMin;
357  valuesIn,
358  coordIn,
359  bigGap,
360  coordOut,
361  DZFrac,
362  method,
363  flagsOut,
364  valuesOut,
365  numGaps,
366  &ZMax,
367  &ZMin);
368 
369  // Compare output values with OPS equivalents.
370  // The name of the OPS variables are hardcoded because they are purely used for testing.
371  // todo(ctgh): check whether any hardcoded names can be substituted.
372  const auto &expected_flagsOut =
373  profileDataHandler.get<int>("OPS_eastward_wind@ModelLevelsQCFlags");
374  for (size_t jlev = 0; jlev < flagsOut.size(); ++jlev)
375  EXPECT(flagsOut[jlev] == expected_flagsOut[jlev]);
376  const auto &expected_valuesOut =
377  profileDataHandler.get<float>("OPS_eastward_wind@ModelLevelsDerivedValue");
378  for (size_t jlev = 0; jlev < flagsOut.size(); ++jlev)
379  EXPECT(oops::is_close_relative(valuesOut[jlev], expected_valuesOut[jlev], 1e-4f));
380  const auto &expected_ZMin =
381  profileDataHandler.get<float>("OPS_LogP_u_Min@ModelLevelsDerivedValue");
382  for (size_t jlev = 0; jlev < ZMin.size(); ++jlev)
383  EXPECT(oops::is_close_relative(ZMin[jlev], expected_ZMin[jlev], 1e-14f));
384  const auto &expected_ZMax =
385  profileDataHandler.get<float>("OPS_LogP_u_Max@ModelLevelsDerivedValue");
386  for (size_t jlev = 0; jlev < ZMax.size(); ++jlev)
387  EXPECT(oops::is_close_relative(ZMax[jlev], expected_ZMax[jlev], 1e-14f));
388  }
389  }
390 
391  // Test the profile data holder.
392  const bool testProfileDataHolder =
393  conf.getBool("testProfileDataHolder", false);
394  if (testProfileDataHolder) {
396  options.deserialize(conf);
397 
398  std::vector<bool> apply(obsspace.nlocs(), true);
399  std::vector<std::vector<bool>> flagged;
400  ProfileDataHandler profileDataHandler(filterdata,
401  options.DHParameters,
402  apply,
403  filtervars,
404  flagged);
405 
406  profileDataHandler.initialiseNextProfile();
407 
408  // Create a ProfileDataHolder and request some variables.
409  ProfileDataHolder profileDataHolder(profileDataHandler);
410  profileDataHolder.fill({ufo::VariableNames::extended_obs_space},
415 
416  // Get GeoVaLs
418 
419  // Attempt to access data with incorrect type.
420  EXPECT_THROWS(profileDataHolder.get<int>(ufo::VariableNames::obs_air_pressure));
421 
422  // Attempt to access nonexistent data.
423  EXPECT_THROWS(profileDataHolder.get<int>("wrong@MetaData"));
424  EXPECT_THROWS(profileDataHolder.getGeoVaLVector("wrong@MetaData"));
425  EXPECT_THROWS(profileDataHolder.getObsDiagVector("wrong@MetaData"));
426 
427  // Check this profile has been marked as being in the correct section of the ObsSpace.
429  EXPECT_THROWS(profileDataHolder.checkObsSpaceSection(ufo::ObsSpaceSection::Extended));
430 
431  // Advance to the profile in the extended section and perform the same check.
432  profileDataHandler.initialiseNextProfile();
433  ProfileDataHolder profileDataHolderExt(profileDataHandler);
434  profileDataHolderExt.fill({ufo::VariableNames::extended_obs_space}, {}, {}, {}, {});
435  EXPECT_THROWS(profileDataHolderExt.checkObsSpaceSection(ufo::ObsSpaceSection::Original));
437 
438  // Exercise the set routine
439  std::vector <int> int1 {1};
440  profileDataHolder.set<int>("test", std::move(int1));
441  std::vector <int> int2 {1};
442  profileDataHolder.set<int>("test", std::move(int2));
443  }
444 }
445 
446 class ConventionalProfileProcessing : public oops::Test {
447  private:
448  std::string testid() const override {return "ufo::test::ConventionalProfileProcessing";}
449 
450  void register_tests() const override {
451  std::vector<eckit::testing::Test>& ts = eckit::testing::specification();
452 
453  const eckit::LocalConfiguration conf(::test::TestEnvironment::config());
454  for (const std::string & testCaseName : conf.keys())
455  {
456  const eckit::LocalConfiguration testCaseConf(::test::TestEnvironment::config(), testCaseName);
457  ts.emplace_back(CASE("ufo/ConventionalProfileProcessing/" + testCaseName, testCaseConf)
458  {
459  testConventionalProfileProcessing(testCaseConf);
460  });
461  }
462  }
463 
464  void clear() const override {}
465 };
466 
467 } // namespace test
468 } // namespace ufo
469 
470 #endif // TEST_UFO_CONVENTIONALPROFILEPROCESSING_H_
Options controlling the operation of the ConventionalProfileProcessing filter.
DataHandlerParameters DHParameters
Parameters related to profile data handler.
ModelParameters ModParameters
Parameters related to the model.
Retrieve and store data for entire sample. This class uses lazy loading; vectors of variables are ret...
std::vector< T > & get(const std::string &fullname)
void priorFilter(const GeoVaLs &) override
void postFilter(const ioda::ObsVector &, const ioda::ObsVector &, const ObsDiagnostics &) override
void preProcess() override
oops::Variables requiredVars() const override
Profile QC: compare geopotential height data against model background values using a Bayesian method....
void runCheck(ProfileDataHandler &profileDataHandler) override
Run check.
Profile QC: compare relative humidity data against model background values using a Bayesian method....
void runCheck(ProfileDataHandler &profileDataHandler) override
Run check.
Profile QC: compare temperature data with model background values using a Bayesian method....
void runCheck(ProfileDataHandler &profileDataHandler) override
Run check.
Profile QC: compare wind speed data against model background values using a Bayesian method....
void runCheck(ProfileDataHandler &profileDataHandler) override
Run check.
Profile QC: reject data which are outside the assimilation time window. Also, if requested,...
void runCheck(ProfileDataHandler &profileDataHandler) override
Run check.
Profile QC check validator.
Retrieve and store data for individual profiles. To do this, first the vector of values in the entire...
std::vector< T > & get(const std::string &fullname)
Profile data holder class.
std::vector< float > & getObsDiagVector(const std::string &fullname)
Retrieve an ObsDiag vector if it is present. If not, throw an exception.
std::vector< T > & get(const std::string &fullname)
Retrieve a vector if it is present. If not, throw an exception.
void set(const std::string &fullname, std::vector< T > &&vec_in)
Set values in a vector.
void fill(const std::vector< std::string > &variableNamesInt, const std::vector< std::string > &variableNamesFloat, const std::vector< std::string > &variableNamesString, const std::vector< std::string > &variableNamesGeoVaLs, const std::vector< std::string > &variableNamesObsDiags)
Fill profile with data.
std::vector< float > & getGeoVaLVector(const std::string &fullname)
Retrieve a GeoVaL vector if it is present. If not, throw an exception.
void checkObsSpaceSection(ufo::ObsSpaceSection section)
Check this profile is in the expected ObsSpace section (original or extended).
@ InterpolationFlag
Interpolation check flag.
@ SuperadiabatFlag
Superadiabatic check flag.
@ HydrostaticFlag
Hydrostatic check flag.
@ PermRejectReport
Blacklisted data.
@ PermRejectFlag
Blacklisted data.
void testConventionalProfileProcessing(const eckit::LocalConfiguration &conf)
ObsPair reverse(const ObsPair &pair)
CASE("ufo/DataExtractor/bilinearinterp/float_linear")
Definition: RunCRTM.h:27
void profileVerticalInterpolation(const std::vector< float > &coordIn, const std::vector< float > &valuesIn, const std::vector< float > &coordOut, std::vector< float > &valuesOut, const ProfileInterpolation::InterpolationMethod interpMethod, const ProfileInterpolation::CoordinateOrder coordOrder, const ProfileInterpolation::OutOfBoundsTreatment outOfBounds)
void calculateVerticalAverage(const std::vector< int > &flagsIn, const std::vector< float > &valuesIn, const std::vector< float > &coordIn, const std::vector< float > &bigGap, const std::vector< float > &coordOut, float DZFrac, ProfileAveraging::Method method, std::vector< int > &flagsOut, std::vector< float > &valuesOut, int &numGaps, std::vector< float > *coordMax, std::vector< float > *coordMin)
Profile vertical averaging onto model levels.
void CalculateModelHeight(const ModelParameters &options, const float orogGeoVaLs, std::vector< float > &zRhoGeoVaLs, std::vector< float > &zThetaGeoVaLs)
Calculate model heights on rho and theta levels. The calculation uses the terrain-following height co...
static constexpr const char *const station_ID
Definition: VariableNames.h:85
static constexpr const char *const extended_obs_space
Definition: VariableNames.h:94
static constexpr const char *const qcflags_observation_report
Definition: VariableNames.h:98
static constexpr const char *const qcflags_eastward_wind
static constexpr const char *const bigPgaps_derived
static constexpr const char *const modellevels_logPWB_rho_derived
static constexpr const char *const qcflags_relative_humidity
static constexpr const char *const geovals_orog
static constexpr const char *const LogP_derived
static constexpr const char *const obs_geopotential_height
Definition: VariableNames.h:23
static constexpr const char *const obs_eastward_wind
Definition: VariableNames.h:21
static constexpr const char *const geovals_pressure
static constexpr const char *const qcflags_time
static constexpr const char *const obs_air_pressure
Definition: VariableNames.h:18
static constexpr const char *const qcflags_air_temperature
Definition: VariableNames.h:99
static constexpr const char *const bkgerr_air_temperature
Definition: VariableNames.h:47
static constexpr const char *const qcflags_geopotential_height