UFO
test/ufo/MetOfficeBuddyPairFinder.h
Go to the documentation of this file.
1 /*
2  * (C) Copyright 2020 Met Office UK
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_METOFFICEBUDDYPAIRFINDER_H_
9 #define TEST_UFO_METOFFICEBUDDYPAIRFINDER_H_
10 
11 #include <iomanip>
12 #include <map>
13 #include <set>
14 #include <string>
15 #include <utility>
16 #include <vector>
17 
18 #include <boost/optional.hpp>
19 
20 #include "eckit/config/LocalConfiguration.h"
21 #include "eckit/testing/Test.h"
22 #include "ioda/ObsSpace.h"
23 #include "ioda/ObsVector.h"
24 #include "oops/mpi/mpi.h"
25 #include "oops/runs/Test.h"
26 #include "oops/util/Expect.h"
27 #include "test/TestEnvironment.h"
30 #include "ufo/utils/StringUtils.h"
31 
32 namespace ufo {
33 namespace test {
34 
35 typedef std::pair<int, int> ObsPair;
36 
37 bool duplicatePairsPresent(const std::vector<ObsPair> &pairs) {
38  bool duplicatesFound = false;
39  std::set<ObsPair> pairSet;
40 
41  // Test for exact duplicates
42  for (const ObsPair &pair : pairs) {
43  bool inserted = pairSet.insert(pair).second;
44  if (!inserted) {
45  duplicatesFound = true;
46  oops::Log::trace() << "Duplicate pair found: (" << pair.first << ", " << pair.second << ")\n";
47  }
48  }
49 
50  // Test for duplicates in reverse order
51  for (const ObsPair &pair : pairs) {
52  ObsPair reversePair(pair.second, pair.first);
53  bool inserted = pairSet.insert(reversePair).second;
54  if (!inserted) {
55  duplicatesFound = true;
56  oops::Log::trace() << "Pair (" << pair.first << ", " << pair.second
57  << ") present also in reverse order\n";
58  }
59  }
60 
61  oops::Log::trace().flush();
62 
63  return duplicatesFound;
64 }
65 
66 template <typename Key, typename Value>
67 Value maxValue(const std::map<Key, Value> &map)
68 {
69  if (map.empty())
70  return Value();
71 
72  typedef typename std::map<Key, Value>::value_type ValueType;
73  return std::max_element(map.begin(), map.end(),
74  [](const ValueType &a, const ValueType &b)
75  { return a.second < b.second; })->second;
76 }
77 
78 int maxTotalNumBuddies(const std::vector<ObsPair> &pairs) {
79  std::map<int, int> numBuddiesByObsId;
80  for (const ObsPair & pair : pairs)
81  ++numBuddiesByObsId[pair.first];
82  return maxValue(numBuddiesByObsId);
83 }
84 
85 int maxNumBuddiesWithSameStationId(const std::vector<ObsPair> &pairs,
86  const std::vector<int> &stationIds) {
87  std::map<int, int> numBuddiesWithSameStationIdByObsId;
88  for (const ObsPair & pair : pairs)
89  if (stationIds[pair.first] == stationIds[pair.second])
90  ++numBuddiesWithSameStationIdByObsId[pair.first];
91  return maxValue(numBuddiesWithSameStationIdByObsId);
92 }
93 
94 void testDuplicatesAndBuddyCountConstraints(const eckit::LocalConfiguration &conf) {
95  util::DateTime bgn(conf.getString("window begin"));
96  util::DateTime end(conf.getString("window end"));
97 
98  const eckit::LocalConfiguration obsSpaceConf(conf, "obs space");
99  ioda::ObsTopLevelParameters obsParams;
100  obsParams.validateAndDeserialize(obsSpaceConf);
101  ioda::ObsSpace obsSpace(obsParams, oops::mpi::world(), bgn, end, oops::mpi::myself());
102 
103  boost::optional<std::vector<float>> airPressures;
104  if (obsSpace.has("MetaData", "air_pressure")) {
105  airPressures = std::vector<float>(obsSpace.nlocs());
106  obsSpace.get_db("MetaData", "air_pressure", *airPressures);
107  }
108 
109  std::vector<float> latitudes(obsSpace.nlocs());
110  obsSpace.get_db("MetaData", "latitude", latitudes);
111 
112  std::vector<float> longitudes(obsSpace.nlocs());
113  obsSpace.get_db("MetaData", "longitude", longitudes);
114 
115  std::vector<util::DateTime> datetimes(obsSpace.nlocs());
116  obsSpace.get_db("MetaData", "datetime", datetimes);
117 
118  std::vector<int> stationIds(obsSpace.recnum().begin(), obsSpace.recnum().end());
119 
120  std::vector<size_t> validObsIds;
121  if (conf.has("valid_obs_ids")) {
122  validObsIds = conf.getUnsignedVector("valid_obs_ids");
123  } else {
124  validObsIds.resize(obsSpace.nlocs());
125  std::iota(validObsIds.begin(), validObsIds.end(), 0);
126  }
127 
128  const eckit::LocalConfiguration modernFilterConf(conf, "Met Office Buddy Check, modern");
129  MetOfficeBuddyCheckParameters modernOptions;
130  modernOptions.deserialize(modernFilterConf);
131 
132  std::vector<ObsPair> modernPairs;
133  {
134  MetOfficeBuddyPairFinder finder(modernOptions, latitudes, longitudes, datetimes,
135  airPressures.get_ptr(), stationIds);
136  const std::vector<MetOfficeBuddyPair> buddyPairs = finder.findBuddyPairs(validObsIds);
137 
138  for (const MetOfficeBuddyPair & pair : buddyPairs) {
139  modernPairs.push_back(ObsPair(pair.obsIdA, pair.obsIdB));
140  }
141  }
142 
143  EXPECT(maxTotalNumBuddies(modernPairs) <= modernOptions.maxTotalNumBuddies);
144  EXPECT(maxNumBuddiesWithSameStationId(modernPairs, stationIds) <=
145  modernOptions.maxNumBuddiesWithSameStationId);
146  EXPECT_NOT(duplicatePairsPresent(modernPairs));
147 
148  const eckit::LocalConfiguration legacyFilterConf(conf, "Met Office Buddy Check, legacy");
149  MetOfficeBuddyCheckParameters legacyOptions;
150  legacyOptions.deserialize(legacyFilterConf);
151 
152  std::vector<ObsPair> legacyPairs;
153  {
154  MetOfficeBuddyPairFinder finder(legacyOptions, latitudes, longitudes, datetimes,
155  airPressures.get_ptr(), stationIds);
156  const std::vector<MetOfficeBuddyPair> buddyPairs = finder.findBuddyPairs(validObsIds);
157 
158  for (const MetOfficeBuddyPair & pair : buddyPairs) {
159  legacyPairs.push_back(ObsPair(pair.obsIdA, pair.obsIdB));
160  }
161  }
162 
163  EXPECT(maxTotalNumBuddies(legacyPairs) <= legacyOptions.maxTotalNumBuddies);
164  EXPECT(maxNumBuddiesWithSameStationId(legacyPairs, stationIds) <=
165  legacyOptions.maxNumBuddiesWithSameStationId);
166  EXPECT_NOT(duplicatePairsPresent(legacyPairs));
167 
168  // The legacy buddy collector sometimes fails to find all observations that can be classified
169  // as buddies while respecting constraints on their maximum number.
170  EXPECT(modernPairs.size() > legacyPairs.size());
171 }
172 
173 CASE("ufo/MetOfficeBuddyPairFinder/"
174  "Duplicates, constraints on buddy counts, legacy pair collector") {
175  testDuplicatesAndBuddyCountConstraints(eckit::LocalConfiguration(
176  ::test::TestEnvironment::config(),
177  "Duplicates, constraints on buddy counts, "
178  "legacy pair collector"));
179 }
180 
181 std::vector<MetOfficeBuddyPair> findBuddyPairs(const MetOfficeBuddyCheckParameters &options,
182  const std::vector<float> &latitudes,
183  const std::vector<float> &longitudes,
184  const std::vector<util::DateTime> &datetimes,
185  const std::vector<float> *pressures,
186  const std::vector<int> &stationIds,
187  const std::vector<size_t> &validObsIds) {
188  MetOfficeBuddyPairFinder finder(options, latitudes, longitudes, datetimes,
189  pressures, stationIds);
190  return finder.findBuddyPairs(validObsIds);
191 }
192 
193 void testInvarianceToLongitude(const eckit::LocalConfiguration &conf) {
194  const float searchRadius = 100; // km
195 
196  std::vector<float> referenceLatitudes = conf.getFloatVector("reference.latitudes");
197  std::vector<float> referenceLongitudes = conf.getFloatVector("reference.longitudes");
198 
199  std::vector<util::DateTime> datetimes(2, util::DateTime());
200  std::vector<int> stationIds{1, 2};
201 
202  std::vector<size_t> validObsIds{0, 1};
203 
204  const eckit::LocalConfiguration filterConf(conf, "Met Office Buddy Check");
206  options.deserialize(filterConf);
207 
208  const std::vector<MetOfficeBuddyPair> referenceBuddyPairs = findBuddyPairs(
209  options, referenceLatitudes, referenceLongitudes,
210  datetimes, nullptr, stationIds, validObsIds);
211  EXPECT_EQUAL(referenceBuddyPairs.size(), 1);
212  const MetOfficeBuddyPair &refPair = referenceBuddyPairs[0];
213 
214  for (const eckit::LocalConfiguration &testConf : conf.getSubConfigurations("test")) {
215  std::vector<float> latitudes = testConf.getFloatVector("latitudes");
216  std::vector<float> longitudes = testConf.getFloatVector("longitudes");
217  const std::vector<MetOfficeBuddyPair> buddyPairs = findBuddyPairs(
218  options, latitudes, longitudes, datetimes, nullptr, stationIds, validObsIds);
219  EXPECT_EQUAL(buddyPairs.size(), 1);
220  const MetOfficeBuddyPair &pair = buddyPairs[0];
221 
222  if (pair.obsIdA == refPair.obsIdA) {
223  EXPECT_EQUAL(pair.obsIdB, refPair.obsIdB);
224  EXPECT(oops::is_close_relative(pair.distanceInKm, refPair.distanceInKm, 1e-6));
225  if (latitudes[pair.obsIdB] - latitudes[pair.obsIdA] ==
226  referenceLatitudes[refPair.obsIdB] - referenceLatitudes[refPair.obsIdA] &&
227  longitudes[pair.obsIdB] - longitudes[pair.obsIdA] ==
228  referenceLongitudes[refPair.obsIdB] - referenceLongitudes[refPair.obsIdA]) {
229  EXPECT(oops::is_close_relative(pair.rotationAInRad, refPair.rotationAInRad, 1e-6));
230  EXPECT(oops::is_close_relative(pair.rotationBInRad, refPair.rotationBInRad, 1e-6));
231  }
232  } else {
233  EXPECT_EQUAL(pair.obsIdA, refPair.obsIdB);
234  EXPECT_EQUAL(pair.obsIdB, refPair.obsIdA);
235  EXPECT(oops::is_close_relative(pair.distanceInKm, refPair.distanceInKm, 1e-6));
236  if (latitudes[pair.obsIdB] - latitudes[pair.obsIdA] ==
237  referenceLatitudes[refPair.obsIdA] - referenceLatitudes[refPair.obsIdB] &&
238  longitudes[pair.obsIdB] - longitudes[pair.obsIdA] ==
239  referenceLongitudes[refPair.obsIdA] - referenceLongitudes[refPair.obsIdB]) {
240  EXPECT(oops::is_close_relative(pair.rotationAInRad, refPair.rotationBInRad, 1e-6));
241  EXPECT(oops::is_close_relative(pair.rotationBInRad, refPair.rotationAInRad, 1e-6));
242  }
243  }
244  }
245 }
246 
247 CASE("ufo/TemporalThinning/Invariance to longitude, different zonal bands") {
248  testInvarianceToLongitude(eckit::LocalConfiguration(
249  ::test::TestEnvironment::config(),
250  "Invariance to longitude, different zonal bands"));
251 }
252 
253 CASE("ufo/TemporalThinning/Invariance to longitude, same zonal band") {
254  testInvarianceToLongitude(eckit::LocalConfiguration(
255  ::test::TestEnvironment::config(),
256  "Invariance to longitude, same zonal band"));
257 }
258 
259 void findEndpoint(double startLat, double startLon, double distance, double bearing,
260  double &endLat, double &endLon)
261 {
262  startLat *= ufo::Constants::deg2rad;
263  startLon *= ufo::Constants::deg2rad;
264  bearing *= ufo::Constants::deg2rad;
266  endLat = std::asin(std::sin(startLat) * std::cos(distance) +
267  std::cos(startLat) * std::sin(distance) * std::cos(bearing));
268  endLon = startLon + std::atan2(std::sin(bearing) * std::sin(distance) * std::cos(startLat),
269  std::cos(distance) - std::sin(startLat) * std::sin(endLat));
270  endLat *= ufo::Constants::rad2deg;
271  endLon *= ufo::Constants::rad2deg;
272 }
273 
274 void findEndpoint(float startLat, float startLon, float distance, float bearing,
275  float &endLat, float &endLon)
276 {
277  double dEndLat, dEndLon;
278  findEndpoint(startLat, startLon, distance, bearing, dEndLat, dEndLon);
279  endLon = dEndLon;
280  endLat = dEndLat;
281 }
282 
283 template <typename T>
284 bool contains(const std::set<T> &set, const T &element) {
285  return set.find(element) != set.end();
286 }
287 
288 ObsPair reverse(const ObsPair &pair) {
289  return ObsPair(pair.second, pair.first);
290 }
291 
292 void testSearchRadius(const eckit::LocalConfiguration &conf) {
293  const eckit::LocalConfiguration filterConf(conf, "Met Office Buddy Check");
295  options.deserialize(filterConf);
296 
297  const float searchRadius = options.searchRadius;
298 
299  for (const eckit::LocalConfiguration &testConf : conf.getSubConfigurations("test")) {
300  float centralLatitude = testConf.getFloat("central_latitude");
301  float centralLongitude = testConf.getFloat("central_longitude");
302 
303  std::vector<float> latitudes{centralLatitude};
304  std::vector<float> longitudes{centralLongitude};
305  std::vector<ObsPair> pairsExpectedToBePresent, pairsExpectedToBeAbsent;
306 
307  const int numBearings = 8;
308  for (float i = 0; i < numBearings; ++i) {
309  float latitude, longitude;
310 
311  findEndpoint(centralLatitude, centralLongitude,
312  0.99f * searchRadius, i * 360.0f / numBearings, latitude, longitude);
313  latitudes.push_back(latitude);
314  longitudes.push_back(longitude);
315  pairsExpectedToBePresent.push_back(ObsPair(0, latitudes.size() - 1));
316 
317  findEndpoint(centralLatitude, centralLongitude,
318  1.01f * searchRadius, i * 360.0f / numBearings, latitude, longitude);
319  latitudes.push_back(latitude);
320  longitudes.push_back(longitude);
321  pairsExpectedToBeAbsent.push_back(ObsPair(0, latitudes.size() - 1));
322  }
323 
324  const std::vector<util::DateTime> datetimes(latitudes.size(), util::DateTime());
325  const std::vector<int> stationIds(latitudes.size(), 0);
326 
327  std::vector<size_t> validObsIds(latitudes.size());
328  std::iota(validObsIds.begin(), validObsIds.end(), 0);
329 
330  const std::vector<MetOfficeBuddyPair> buddyPairs = findBuddyPairs(
331  options, latitudes, longitudes, datetimes, nullptr, stationIds, validObsIds);
332 
333  std::set<ObsPair> pairs;
334  for (const MetOfficeBuddyPair & pair : buddyPairs) {
335  pairs.insert(ObsPair(pair.obsIdA, pair.obsIdB));
336  }
337 
338  for (const ObsPair &pair : pairsExpectedToBePresent)
339  EXPECT(contains(pairs, pair) || contains(pairs, reverse(pair)));
340 
341  for (const ObsPair &pair : pairsExpectedToBeAbsent)
342  EXPECT(!contains(pairs, pair) && !contains(pairs, reverse(pair)));
343  }
344 }
345 
346 CASE("ufo/TemporalThinning/Search radius") {
347  testSearchRadius(eckit::LocalConfiguration(::test::TestEnvironment::config(), "Search radius"));
348 }
349 
350 class MetOfficeBuddyPairFinder : public oops::Test {
351  private:
352  std::string testid() const override {return "ufo::test::MetOfficeBuddyPairFinder";}
353 
354  void register_tests() const override {}
355 
356  void clear() const override {}
357 };
358 
359 } // namespace test
360 } // namespace ufo
361 
362 #endif // TEST_UFO_METOFFICEBUDDYPAIRFINDER_H_
Options controlling the operation of the MetOfficeBuddyCheck filter.
oops::Parameter< float > searchRadius
Maximum distance between two observations that may be classified as buddies, in km.
float distance(const Point &a, const Point &b)
Returns the distance between the two cartesian-mapped Point arguments
void testSearchRadius(const eckit::LocalConfiguration &conf)
std::pair< int, int > ObsPair
Value maxValue(const std::map< Key, Value > &map)
int maxTotalNumBuddies(const std::vector< ObsPair > &pairs)
int maxNumBuddiesWithSameStationId(const std::vector< ObsPair > &pairs, const std::vector< int > &stationIds)
bool duplicatePairsPresent(const std::vector< ObsPair > &pairs)
ObsPair reverse(const ObsPair &pair)
CASE("ufo/DataExtractor/bilinearinterp/float_linear")
void findEndpoint(double startLat, double startLon, double distance, double bearing, double &endLat, double &endLon)
void testInvarianceToLongitude(const eckit::LocalConfiguration &conf)
void testDuplicatesAndBuddyCountConstraints(const eckit::LocalConfiguration &conf)
std::vector< MetOfficeBuddyPair > findBuddyPairs(const MetOfficeBuddyCheckParameters &options, const std::vector< float > &latitudes, const std::vector< float > &longitudes, const std::vector< util::DateTime > &datetimes, const std::vector< float > *pressures, const std::vector< int > &stationIds, const std::vector< size_t > &validObsIds)
bool contains(const std::set< T > &set, const T &element)
Definition: RunCRTM.h:27
static constexpr double deg2rad
Definition: Constants.h:21
static constexpr double rad2deg
Definition: Constants.h:22
static constexpr double mean_earth_rad
Definition: Constants.h:40
Properties of a pair of observations checked against each other during buddy check.