From 674b2264e94906d5bb1fb2a30b684d8974ecc0b1 Mon Sep 17 00:00:00 2001 From: Thomas Nipen Date: Mon, 11 Mar 2024 11:48:16 +0100 Subject: [PATCH] More unit tests --- src/api/curve.cpp | 5 ++++ src/api/transform.cpp | 9 +++--- tests/test_apply_curve.py | 63 ++++++++++++++++++++++++++++++++------- tests/test_fill.py | 1 + tests/test_humidity.py | 26 ++++++++++++---- tests/test_qnh.py | 35 ++++++++++++++++++++++ tests/test_transform.py | 26 ++++++++++++++++ tests/test_util.py | 5 ++++ 8 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 tests/test_qnh.py diff --git a/src/api/curve.cpp b/src/api/curve.cpp index c7309ee0..1f0ba264 100644 --- a/src/api/curve.cpp +++ b/src/api/curve.cpp @@ -4,6 +4,11 @@ using namespace gridpp; float gridpp::apply_curve(float input, const vec& curve_ref, const vec& curve_fcst, gridpp::Extrapolation policy_below, gridpp::Extrapolation policy_above) { + if(curve_ref.size() != curve_fcst.size()) + throw std::invalid_argument("curve_ref and curve_fcst must be the same size"); + if(curve_ref.size() == 0 || curve_ref.size() == 0) + throw std::invalid_argument("curve_ref and curve_fcst cannot have size 0"); + int C = curve_fcst.size(); float smallestObs = curve_ref[0]; float smallestFcst = curve_fcst[0]; diff --git a/src/api/transform.cpp b/src/api/transform.cpp index 1ecf1dd7..682b7735 100644 --- a/src/api/transform.cpp +++ b/src/api/transform.cpp @@ -3,6 +3,7 @@ using namespace gridpp; +// These two are included because of SWIG. See note about SWIG requirement in gridpp.h float gridpp::Transform::forward(float value) const { return -1; } @@ -125,12 +126,12 @@ float gridpp::BoxCox::backward(float value) const { gridpp::Gamma::Gamma(float shape, float scale, float tolerance) : m_gamma_dist(1, 1), m_norm_dist(), m_tolerance(tolerance) { // Initialize the gamma distribution to something that works, and then overwrite it so that // we can check for argument errors gracefully - if(shape <= 0) + if(!gridpp::is_valid(shape) || shape <= 0) throw std::invalid_argument("Shape parameter must be > 0 in the gamma distribution"); - if(scale <= 0) + if(!gridpp::is_valid(scale) || scale <= 0) throw std::invalid_argument("Scale parameter must be > 0 in the gamma distribution"); - if(tolerance < 0) - throw std::invalid_argument("Tolerance must be > 0 in the gamma distribution"); + if(!gridpp::is_valid(tolerance) || tolerance < 0) + throw std::invalid_argument("Tolerance must be >= 0 in the gamma distribution"); m_gamma_dist = boost::math::gamma_distribution<> (shape, scale); } float gridpp::Gamma::forward(float value) const { diff --git a/tests/test_apply_curve.py b/tests/test_apply_curve.py index 839f6809..614f1276 100644 --- a/tests/test_apply_curve.py +++ b/tests/test_apply_curve.py @@ -8,25 +8,54 @@ class Test(unittest.TestCase): def test_empty_curve(self): """Check for exception on empty curve""" - with self.assertRaises(Exception) as e: - gridpp.apply_curve([0, 1], [[], []], gridpp.OneToOne, gridpp.OneToOne) - with self.assertRaises(Exception) as e: - gridpp.apply_curve([0, 1], [[1, 2], []], gridpp.OneToOne, gridpp.OneToOne) - with self.assertRaises(Exception) as e: - gridpp.apply_curve([0, 1], [[], [1, 2]], gridpp.OneToOne, gridpp.OneToOne) + inputs = [0, [0, 1], [[0], [1]]] + for input in inputs: + with self.subTest(input=input): + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [], [], gridpp.OneToOne, gridpp.OneToOne) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [1, 2], [], gridpp.OneToOne, gridpp.OneToOne) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [], [1, 2], gridpp.OneToOne, gridpp.OneToOne) def test_invalid_curve(self): """Check for exception on invalid curve""" - with self.assertRaises(Exception) as e: - gridpp.apply_curve([0, 1], [1, 2, 3], [1, 2], gridpp.OneToOne, gridpp.OneToOne) - with self.assertRaises(Exception) as e: - gridpp.apply_curve([0, 1], [1, 2], [1, 2, 3], gridpp.OneToOne, gridpp.OneToOne) + inputs = [0, [0, 1], [[0], [1]]] + for input in inputs: + with self.subTest(input=input): + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [1, 2, 3], [1, 2], gridpp.OneToOne, gridpp.OneToOne) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [1, 2], [1, 2, 3], gridpp.OneToOne, gridpp.OneToOne) + + def test_invalid_3d_curve(self): + input0 = np.reshape(np.arange(8), [2, 4]) + input = np.reshape(np.arange(6), [2, 3]) + x0 = np.reshape(np.arange(18), [2, 3, 3]) + y0 = np.reshape(np.arange(18), [2, 3, 3]) + x = np.reshape(np.arange(24), [2, 3, 4]) + y = np.reshape(np.arange(24), [2, 3, 4]) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input0, x, y, gridpp.OneToOne, gridpp.OneToOne) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, x0, y, gridpp.OneToOne, gridpp.OneToOne) + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, x, y0, gridpp.OneToOne, gridpp.OneToOne) + + # Test empty curve + with self.assertRaises(ValueError) as e: + gridpp.apply_curve(input, [[]], [[]], gridpp.OneToOne, gridpp.OneToOne) def test_empty_fcst(self): """Check for empty result on empty input""" + # 1D input q = gridpp.apply_curve([], [1, 2], [1, 2], gridpp.OneToOne, gridpp.OneToOne) np.testing.assert_array_equal(q, []) + # 2D input + q = gridpp.apply_curve([[]], [1, 2], [1, 2], gridpp.OneToOne, gridpp.OneToOne) + np.testing.assert_array_equal(q, [[]]) + def test_edge(self): """Check values on edge of curve""" x = [1, 2, 3] @@ -37,7 +66,7 @@ def test_edge(self): output = gridpp.apply_curve([val], y, x, policy, policy) np.testing.assert_array_equal(output, [val+1]) - def test_extrapolation(self): + def test_extrapolation_1d(self): """Check values outside curve""" x = [1, 2, 3] y = [2, 5, 6] @@ -48,6 +77,18 @@ def test_extrapolation(self): np.testing.assert_array_equal(gridpp.apply_curve([0,4], y, x, gridpp.NearestSlope, gridpp.NearestSlope), [-1,7]) np.testing.assert_array_equal(gridpp.apply_curve([0,4], y, x, gridpp.Unchanged, gridpp.Unchanged), [0,4]) + def test_extrapolation_2d(self): + """Check values outside curve""" + x = [1, 2, 3] + y = [2, 5, 6] + input = [[0],[4]] + policies = [gridpp.OneToOne, gridpp.Zero, gridpp.MeanSlope, gridpp.NearestSlope] + np.testing.assert_array_equal(gridpp.apply_curve(input, y, x, gridpp.OneToOne, gridpp.OneToOne), [[1],[7]]) + np.testing.assert_array_equal(gridpp.apply_curve(input, y, x, gridpp.Zero, gridpp.Zero), [[2],[6]]) + np.testing.assert_array_equal(gridpp.apply_curve(input, y, x, gridpp.MeanSlope, gridpp.MeanSlope), [[0],[8]]) + np.testing.assert_array_equal(gridpp.apply_curve(input, y, x, gridpp.NearestSlope, gridpp.NearestSlope), [[-1],[7]]) + np.testing.assert_array_equal(gridpp.apply_curve(input, y, x, gridpp.Unchanged, gridpp.Unchanged), [[0],[4]]) + def test_3d(self): curve_fcst = np.random.rand(3, 2, 4) curve_ref = np.random.rand(3, 2, 4) diff --git a/tests/test_fill.py b/tests/test_fill.py index 4d7c8222..a45d7837 100644 --- a/tests/test_fill.py +++ b/tests/test_fill.py @@ -20,6 +20,7 @@ def test_invalid_radii(self): def test_invalid_number_of_radii(self): values = np.zeros([3, 3]) + value = 1 for outside in [False, True]: with self.assertRaises(Exception) as e: gridpp.fill(grid, values, points, [1], value, outside) diff --git a/tests/test_humidity.py b/tests/test_humidity.py index 33fd488c..714fdab5 100644 --- a/tests/test_humidity.py +++ b/tests/test_humidity.py @@ -17,9 +17,9 @@ def test_invalid_input(self): def test_relative_humidity(self): # NOTE: RH above > 100 C are 1 in the implementation - t = [293.15, 293.15, 300, 400] - td = [293.15, 289.783630, 300, 370] - rh = [1, 0.817590594291687, 1, 1] + t = [270, 270, 293.15, 293.15, 300, 400] + td = [160, 260, 293.15, 289.783630, 300, 370] + rh = [0, 0.4605, 1, 0.817590594291687, 1, 1] for i in range(len(t)): self.assertAlmostEqual(gridpp.relative_humidity(t[i], td[i]), rh[i], 4) np.testing.assert_almost_equal(gridpp.relative_humidity(t, td), rh, 4) @@ -37,6 +37,8 @@ def test_dewpoint(self): td = [293.15, 289.783630, 300] for i in range(len(t)): self.assertAlmostEqual(gridpp.dewpoint(t[i], rh[i]), td[i], 4) + + # Vector version np.testing.assert_almost_equal(gridpp.dewpoint(t, rh), td, 4) def test_dewpoint_invalid(self): @@ -56,13 +58,25 @@ def test_wetbulb(self): np.testing.assert_almost_equal(gridpp.wetbulb(t, p, rh), ans, 4) def test_wetbulb_invalid(self): - t = [np.nan, np.nan, 293.15, 293.15, np.nan, 293.15] - p = [101325, 101325, 101325, np.nan, np.nan, np.nan] - rh = [0.9, np.nan, np.nan, 0.9, np.nan, 0] + t = [np.nan, np.nan, 293.15, 293.15, np.nan, 293.15, 273.15] + p = [101325, 101325, 101325, np.nan, np.nan, np.nan, 0] + rh = [0.9, np.nan, np.nan, 0.9, np.nan, 0, 0] for i in range(len(t)): self.assertTrue(np.isnan(gridpp.wetbulb(t[i], p[i], rh[i]))) self.assertTrue(np.isnan(gridpp.wetbulb(t, p, rh)).all()) + def test_wetbulb_invalid_arguments(self): + t = [273, 274, 275] + p = [101325, 101325, 101325] + rh = [0.5, 0.5, 0.5] + with self.assertRaises(ValueError) as e: + gridpp.wetbulb(t[1:], p, rh) + with self.assertRaises(ValueError) as e: + gridpp.wetbulb(t, p[1:], rh) + with self.assertRaises(ValueError) as e: + gridpp.wetbulb(t, p, rh[1:]) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_qnh.py b/tests/test_qnh.py new file mode 100644 index 00000000..660ad700 --- /dev/null +++ b/tests/test_qnh.py @@ -0,0 +1,35 @@ +from __future__ import print_function +import unittest +import gridpp +import numpy as np +import os + + +class Test(unittest.TestCase): + def test_invalid_input(self): + """Check that dimension missmatch results in error""" + with self.assertRaises(Exception) as e: + gridpp.qnh([101325], [0, 20]) + + def test_invalid_values(self): + self.assertTrue(np.isnan(gridpp.qnh([-1], [0]))) + + def test_1(self): + p = [101325, 90000, 90000, 110000] + alt = [0, 1000, 0, -1000] + expected = [101325, 101463.21875, 90000, 97752.90742927508] + for i in range(len(p)): + self.assertAlmostEqual(gridpp.qnh(p[i], alt[i]), expected[i], 1) + np.testing.assert_almost_equal(gridpp.qnh(p, alt), expected, 1) + + def test_no_pressure(self): + for altitude in [-1000, 0, 1000]: + self.assertEqual(gridpp.qnh([0], [altitude]), [0]) + self.assertEqual(gridpp.qnh(0, altitude), 0) + + def test_empty(self): + np.testing.assert_almost_equal(gridpp.qnh([],[]), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_transform.py b/tests/test_transform.py index 60adac1e..cae2a9f5 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -85,6 +85,32 @@ def test_gamma(self): output = transform.backward(a) np.testing.assert_almost_equal(output, i, 5) + def test_gamma_nan_value(self): + transform = gridpp.Gamma(1, 2, 0.01) + self.assertTrue(np.isnan(transform.forward(np.nan))) + self.assertTrue(np.isnan(transform.backward(np.nan))) + self.assertTrue(np.isnan(transform.forward([np.nan])).all()) + self.assertTrue(np.isnan(transform.backward([np.nan])).all()) + + + def test_gamma_tolerance0(self): + transform = gridpp.Gamma(1, 2, 0) + + def test_gamma_invalid_arguments(self): + """Test exception when shape and/or scale are 0 or less""" + for value in [-1, 0, np.nan]: + with self.assertRaises(ValueError) as e: + transform = gridpp.Gamma(value, 2, 0.01) + with self.assertRaises(ValueError) as e: + transform = gridpp.Gamma(2, value, 0.01) + with self.assertRaises(ValueError) as e: + transform = gridpp.Gamma(value, value, 0.01) + + # Tolerance must be >= 0 + for value in [-1, np.nan]: + with self.assertRaises(ValueError) as e: + transform = gridpp.Gamma(1, 2, value) + def test_zero_size(self): x = np.zeros([0, 1]) transform = gridpp.Identity() diff --git a/tests/test_util.py b/tests/test_util.py index a26be346..278d508e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -182,5 +182,10 @@ def test_is_valid_lon(self): with self.subTest(v=v): self.assertFalse(gridpp.is_valid_lon(v, gridpp.Geodetic)) + def test_set_debug_level(self): + for level in [0, 1, 10]: + gridpp.set_debug_level(level) + self.assertEqual(level, gridpp.get_debug_level()) + if __name__ == '__main__': unittest.main()