diff --git a/src/aero_mode.hpp b/src/aero_mode.hpp index 2f1a1e1d..ed69cb89 100644 --- a/src/aero_mode.hpp +++ b/src/aero_mode.hpp @@ -148,6 +148,20 @@ struct AeroMode { if (mode["mode_type"] == "sampled") { if (mode.find("size_dist") == mode.end()) throw std::runtime_error("size_dist key must be set for mode_type=sampled"); + auto sd = mode["size_dist"]; + if ( + sd.size() != 2 || + !sd[0].is_object() || + sd[0].size() != 1 || + sd[1].size() != 1 || + sd[0].find("num_conc") == sd[0].end() || + sd[1].find("diam") == sd[1].end() + ) + throw std::runtime_error("size_dist value must be an iterable of two single-element dicts (first with 'num_conc', second with 'diam' as keys)"); + auto num_conc = *sd[0].find("num_conc"); + auto diam = *sd[1].find("diam"); + if (diam.size() != num_conc.size() + 1) + throw std::runtime_error("size_dist['num_conc'] must have len(size_dist['diam'])-1 elements"); } } diff --git a/tests/test_aero_dist.py b/tests/test_aero_dist.py index adb64cac..790c9f2e 100644 --- a/tests/test_aero_dist.py +++ b/tests/test_aero_dist.py @@ -217,3 +217,22 @@ def test_ctor_error_on_repeated_massfrac_keys(): # assert assert str(exc_info.value) == "mass_frac keys must be unique" + + @staticmethod + def test_ctor_sampled_mode(): + # arrange + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + ctor_arg = copy.deepcopy(AERO_DIST_CTOR_ARG_MINIMAL) + ctor_arg[0]["test_mode"]["mode_type"] = "sampled" + ctor_arg[0]["test_mode"]["size_dist"] = [ + {"num_conc": [1, 2, 3]}, + {"diam": [1, 2, 3, 4]}, + ] + + # act + sut = ppmc.AeroDist(aero_data, ctor_arg) + + # assert + assert sut.mode(0).num_conc == sum( + ctor_arg[0]["test_mode"]["size_dist"][0]["num_conc"] + ) diff --git a/tests/test_aero_mode.py b/tests/test_aero_mode.py index fbcc026d..250af41c 100644 --- a/tests/test_aero_mode.py +++ b/tests/test_aero_mode.py @@ -304,6 +304,57 @@ def test_sampled_without_size_dist(): # assert assert str(exc_info.value) == "size_dist key must be set for mode_type=sampled" + @staticmethod + @pytest.mark.parametrize( + "fishy", + ( + None, + [], + [{}, {}, {}], + [{}, []], + [{"diam": None}, {}], + [{"num_conc": None}, {}], + [{"diam": None, "": None}, {}], + [{"num_conc": None, "": None}, {}], + ), + ) + def test_sampled_with_fishy_size_dist(fishy): + # arrange + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + fishy_ctor_arg = copy.deepcopy(AERO_MODE_CTOR_LOG_NORMAL) + fishy_ctor_arg["test_mode"]["mode_type"] = "sampled" + fishy_ctor_arg["test_mode"]["size_dist"] = fishy + + # act + with pytest.raises(Exception) as exc_info: + ppmc.AeroMode(aero_data, fishy_ctor_arg) + + # assert + assert ( + str(exc_info.value) + == "size_dist value must be an iterable of two single-element dicts (first with 'num_conc', second with 'diam' as keys)" + ) + + def test_sampled_with_diam_of_different_len_than_num_conc(fishy): + # arrange + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + fishy_ctor_arg = copy.deepcopy(AERO_MODE_CTOR_LOG_NORMAL) + fishy_ctor_arg["test_mode"]["mode_type"] = "sampled" + fishy_ctor_arg["test_mode"]["size_dist"] = [ + {"num_conc": [1, 2, 3]}, + {"diam": [1, 2, 3]}, + ] + + # act + with pytest.raises(Exception) as exc_info: + ppmc.AeroMode(aero_data, fishy_ctor_arg) + + # assert + assert ( + str(exc_info.value) + == "size_dist['num_conc'] must have len(size_dist['diam'])-1 elements" + ) + @staticmethod def test_sampled(): # arrange @@ -319,8 +370,8 @@ def test_sampled(): "diam_type": "geometric", "mode_type": "sampled", "size_dist": [ - {"diam": [1, 2, 3, 4]}, {"num_conc": num_concs}, + {"diam": [1, 2, 3, 4]}, ], } },