diff --git a/src/aero_dist.F90 b/src/aero_dist.F90 index 65faec8d..4d729cfb 100644 --- a/src/aero_dist.F90 +++ b/src/aero_dist.F90 @@ -40,4 +40,36 @@ subroutine f_aero_dist_from_json(ptr_c, aero_data_ptr_c) bind(C) call spec_file_read_aero_dist(file, aero_data_ptr_f, aero_dist) end subroutine + + subroutine f_aero_dist_n_mode(ptr_c, n_mode) bind(C) + type(aero_dist_t), pointer :: aero_dist => null() + type(c_ptr), intent(in) :: ptr_c + integer(c_int), intent(out) :: n_mode + + call c_f_pointer(ptr_c, aero_dist) + n_mode = aero_dist_n_mode(aero_dist) + end subroutine + + subroutine f_aero_dist_total_num_conc(ptr_c, total_num_conc) bind(C) + type(aero_dist_t), pointer :: aero_dist => null() + type(c_ptr), intent(in) :: ptr_c + real(c_double), intent(out) :: total_num_conc + + call c_f_pointer(ptr_c, aero_dist) + total_num_conc = aero_dist_total_num_conc(aero_dist) + end subroutine + + subroutine f_aero_dist_mode(ptr_c, aero_mode_ptr_c, index) bind(C) + type(c_ptr) :: ptr_c, aero_mode_ptr_c + type(aero_dist_t), pointer :: aero_dist + type(aero_mode_t), pointer :: aero_mode + integer(c_int), intent(in) :: index + + call c_f_pointer(ptr_c, aero_dist) + call c_f_pointer(aero_mode_ptr_c, aero_mode) + + aero_mode = aero_dist%mode(index + 1) + + end subroutine + end module diff --git a/src/aero_dist.hpp b/src/aero_dist.hpp index f2058283..d4c25889 100644 --- a/src/aero_dist.hpp +++ b/src/aero_dist.hpp @@ -7,6 +7,7 @@ #pragma once #include "pmc_resource.hpp" +#include "aero_mode.hpp" extern "C" void f_aero_dist_ctor( void *ptr @@ -16,19 +17,62 @@ extern "C" void f_aero_dist_dtor( void *ptr ) noexcept; +extern "C" void f_aero_dist_n_mode( + const void *ptr, + int *n_mode +) noexcept; + extern "C" void f_aero_dist_from_json( void *ptr, void *aero_data_ptr ) noexcept; +extern "C" void f_aero_dist_total_num_conc( + const void *ptr, + double *total_num_conc +) noexcept; + +extern "C" void f_aero_dist_mode( + const void *ptr, + void *ptr_c, + const int *index +) noexcept; + struct AeroDist { PMCResource ptr; + std::shared_ptr aero_data; - AeroDist(AeroData &aero_data, const nlohmann::json &json): - ptr(f_aero_dist_ctor, f_aero_dist_dtor) + AeroDist( + std::shared_ptr aero_data, + const nlohmann::json &json + ): + ptr(f_aero_dist_ctor, f_aero_dist_dtor), + aero_data(aero_data) { gimmick_ptr() = std::make_unique(json, "", "mode_name", 1); - f_aero_dist_from_json(ptr.f_arg_non_const(), aero_data.ptr.f_arg_non_const()); + f_aero_dist_from_json(ptr.f_arg_non_const(), aero_data->ptr.f_arg_non_const()); gimmick_ptr().reset(); } + + static auto get_n_mode(const AeroDist &self) { + int n_mode; + f_aero_dist_n_mode(self.ptr.f_arg(), &n_mode); + return n_mode; + } + + static auto get_total_num_conc(const AeroDist &self) { + double total_num_conc; + f_aero_dist_total_num_conc(self.ptr.f_arg(), &total_num_conc); + return total_num_conc; + } + + static AeroMode* get_mode(const AeroDist &self, const int &idx) { + if (idx < 0 || idx >= AeroDist::get_n_mode(self)) + throw std::out_of_range("Index out of range"); + + AeroMode *ptr = new AeroMode(); + f_aero_dist_mode(self.ptr.f_arg(), ptr, &idx); + + return ptr; + } }; diff --git a/src/aero_mode.hpp b/src/aero_mode.hpp index a5bfd2e0..3c8c3a1b 100644 --- a/src/aero_mode.hpp +++ b/src/aero_mode.hpp @@ -109,6 +109,10 @@ extern "C" void f_aero_mode_from_json( struct AeroMode { PMCResource ptr; + AeroMode() : + ptr(f_aero_mode_ctor, f_aero_mode_dtor) + {} + AeroMode(AeroData &aero_data, const nlohmann::json &json) : ptr(f_aero_mode_ctor, f_aero_mode_dtor) { @@ -230,27 +234,30 @@ struct AeroMode { } static auto types() { - static auto vec = std::vector({ + static auto vec = std::vector{ "log_normal", "exp", "mono", "sampled" - }); + }; return vec; }; static void set_type(AeroMode &self, const std::string &mode_type) { - auto it = std::find( - AeroMode::types().begin(), - AeroMode::types().end(), - mode_type - ); - - if (it == AeroMode::types().end()) + auto found = false; + auto type = 0; + + for (auto el : AeroMode::types()) { + ++type; + if (el == mode_type) { + found = true; + break; + } + } + + if (!found) throw std::invalid_argument("Invalid mode type."); - int type = 1 + std::distance(AeroMode::types().begin(), it); - f_aero_mode_set_type(self.ptr.f_arg_non_const(), &type); } diff --git a/src/aero_state.hpp b/src/aero_state.hpp index bbe7cb7c..5f4d7579 100644 --- a/src/aero_state.hpp +++ b/src/aero_state.hpp @@ -332,12 +332,13 @@ struct AeroState { const AeroState &self, const int &idx ) { + if (idx < 0 || idx >= (int)__len__(self)) + throw std::out_of_range("Index out of range"); + int len = AeroData::__len__(*self.aero_data); std::valarray data(len); AeroParticle *ptr = new AeroParticle(self.aero_data, data); - if (idx < 0 || idx >= (int)__len__(self)) - throw std::out_of_range("Index out of range"); f_aero_state_particle(self.ptr.f_arg(), ptr, &idx); return ptr; diff --git a/src/fake_spec_file.cpp b/src/fake_spec_file.cpp index 8c7a9780..3b38e9e8 100644 --- a/src/fake_spec_file.cpp +++ b/src/fake_spec_file.cpp @@ -217,10 +217,20 @@ void c_spec_file_read_line( std::string name, data; *eof = gimmick_ptr()->read_line(name, data); - int i = 0; - for (const auto &ch : name) { - assert(i < *name_size); - name_data[i++] = ch; + { + int i = 0; + for (const auto &ch : name) { + assert(i < *name_size); + name_data[i++] = ch; + } + *name_size = i; + } + { + int i = 0; + for (const auto &ch : data) { + assert(i < *data0_size); + data0_data[i++] = ch; + } + *data0_size = i; } - *name_size = i; } diff --git a/src/gimmicks.hpp b/src/gimmicks.hpp index c45d54fc..6ad8d8c9 100644 --- a/src/gimmicks.hpp +++ b/src/gimmicks.hpp @@ -43,13 +43,21 @@ struct Gimmick { return this->json->dump(); } - std::string last_dict_key() const noexcept { - std::string key = ""; - for (const auto& item : this->json->items()) - { + template + std::string next_dict_key(T last_key, const T key_cond) const noexcept { + std::string key = "", prev_key = ""; + if (last_key == key_cond) + last_key = ""; + + for (const auto& item : this->json->items()) { if (this->json->is_array()) { for (auto &entry : item.value().items()) { - key = entry.key(); + if (prev_key == last_key) { + key = entry.key(); + break; + } + else + prev_key = entry.key(); } } else { key = item.key(); @@ -63,7 +71,7 @@ struct Gimmick { void zoom_in(const bpstd::string_view &sub) noexcept { auto it = this->json->is_array() - ? this->json->at(this->json->size()-1).begin() + ? this->json->at(this->json->size()-1).find(sub) : this->json->find(sub); // TODO #112: handle errors this->json_parent.push(this->json); @@ -86,35 +94,29 @@ struct Gimmick { } // TODO #112: to be removed after initialising GasData with a list, and not JSON? - std::string first_field_name() const noexcept { + auto first_field_name() const noexcept { // TODO #112: handle errors + std::string name = ""; assert(this->json->size() > 0); assert(this->json->begin()->size() > 0); for (auto &entry : this->json->at(0).items()) { - return entry.key(); + name = entry.key(); } - assert(false); - return ""; - } - - auto is_empty() noexcept { - return this->json->empty(); - } - - auto size() noexcept { - return this->json->size(); + if (name == "") + assert(false); + return name; } - std::size_t n_elements(const bpstd::string_view &name) noexcept { + auto n_elements(const bpstd::string_view &name) noexcept { + std::size_t n_elem = 0; for (auto i=0u; ijson->size(); ++i) { for (auto &entry : this->json->at(i).items()) { if (entry.key() == name) - return entry.value().size(); + n_elem = entry.value().size(); } } - assert(false); - return 0; + return n_elem; } auto n_numeric_array_entries() noexcept { @@ -223,26 +225,30 @@ struct InputGimmick: Gimmick { } bool read_line(std::string &name, std::string &data) { - bool eof = this->is_empty(); - - if (this->zoom_level() == this->max_zoom_level) { // TODO #112 - eof = true; + bool subsequent_record = false; + if (this->zoom_level() == this->max_zoom_level) { this->zoom_out(); - } - if (!eof) { - assert(this->size() == 1); - auto key = this->last_dict_key(); - if (this->key_name != "" && (this->key_cond == this->last_read_line_key)) { - name = this->key_name; - this->zoom_in(key); - } else { - name = key; + auto key = this->next_dict_key(this->last_read_line_key, this->key_cond); + if (key == "") { + this->last_read_line_key = ""; + return true; } - data = key; - this->last_read_line_key = key; + else + subsequent_record = true; } - return eof; + + auto key = this->next_dict_key(this->last_read_line_key, this->key_cond); + if (subsequent_record || (this->key_name != "" && (this->key_cond == this->last_read_line_key))) { + name = this->key_name; + this->zoom_in(key); + } else { + name = key; + } + data = key; + this->last_read_line_key = key; + + return false; } }; diff --git a/src/pypartmc.cpp b/src/pypartmc.cpp index d1c8b53b..b2e80845 100644 --- a/src/pypartmc.cpp +++ b/src/pypartmc.cpp @@ -326,7 +326,11 @@ PYBIND11_MODULE(_PyPartMC, m) { ; py::class_(m,"AeroDist") - .def(py::init()) + .def(py::init, const nlohmann::json&>()) + .def_property_readonly("n_mode", &AeroDist::get_n_mode) + .def_property_readonly("num_conc", &AeroDist::get_total_num_conc) + .def("mode", AeroDist::get_mode, + "returns the mode of a given index") ; m.def( diff --git a/tests/test_aero_dist.py b/tests/test_aero_dist.py index 0760da3f..de52b731 100644 --- a/tests/test_aero_dist.py +++ b/tests/test_aero_dist.py @@ -4,6 +4,12 @@ # Authors: https://github.com/open-atmos/PyPartMC/graphs/contributors # #################################################################################################### +import copy +import gc + +import numpy as np +import pytest + import PyPartMC as ppmc from .test_aero_data import AERO_DATA_CTOR_ARG_MINIMAL @@ -14,6 +20,15 @@ ] +@pytest.fixture +def sut_minimal(): + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + sut = ppmc.AeroDist(aero_data, AERO_DIST_CTOR_ARG_MINIMAL) + aero_data = None + gc.collect() + return sut + + # pylint: disable=too-few-public-methods class TestAeroDist: @staticmethod @@ -26,3 +41,71 @@ def test_ctor(): # assert assert sut is not None + + @staticmethod + @pytest.mark.parametrize("n_modes", (1, 2, 3)) + def test_ctor_multimode(n_modes): + # arrange + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + + mode_data = AERO_DIST_CTOR_ARG_MINIMAL[0]["test_mode"] + num_concs = np.random.rand(n_modes) + modes = {} + for k in range(n_modes): + mode_data["num_conc"] = num_concs[k] + modes[k] = copy.deepcopy(mode_data) + + # act + sut = ppmc.AeroDist( + aero_data, [{f"mode_{k}": modes[k] for k in range(n_modes)}] + ) + + # assert + assert sut.n_mode == n_modes + assert sut.num_conc == np.sum(num_concs) + for i in range(sut.n_mode): + assert sut.mode(i).type == modes[i]["mode_type"] + assert sut.mode(i).num_conc == modes[i]["num_conc"] + + @staticmethod + @pytest.mark.parametrize("idx", (-1, 500)) + def test_get_mode_out_of_range( + sut_minimal, idx + ): # pylint: disable=redefined-outer-name + # act + try: + _ = sut_minimal.mode(idx) + except IndexError: + return + + # assert + assert False + + @staticmethod + def test_get_mode_result_lifetime( + sut_minimal, + ): # pylint: disable=redefined-outer-name + # arrange + mode = sut_minimal.mode(0) + mode_type = mode.type + + # act + sut_minimal = None + gc.collect() + + # assert + assert mode.type == mode_type + + @staticmethod + def test_get_mode_is_a_copy(sut_minimal): # pylint: disable=redefined-outer-name + # arrange + new_type = "mono" + mode_idx = 0 + mode = sut_minimal.mode(mode_idx) + assert mode.type != new_type + + # act + mode.type = new_type + + # assert + assert sut_minimal.mode(mode_idx).type != new_type diff --git a/tests/test_aero_state.py b/tests/test_aero_state.py index 50367625..2b394035 100644 --- a/tests/test_aero_state.py +++ b/tests/test_aero_state.py @@ -203,7 +203,7 @@ def test_different_particles(sut_minimal): # pylint: disable=redefined-outer-na @staticmethod @pytest.mark.parametrize("idx", (-1, 500)) - def test_get_item_out_of_range( + def test_get_particle_out_of_range( sut_minimal, idx ): # pylint: disable=redefined-outer-name # act diff --git a/tests/test_scenario.py b/tests/test_scenario.py index fff81356..edb7a36f 100644 --- a/tests/test_scenario.py +++ b/tests/test_scenario.py @@ -132,3 +132,18 @@ def test_init_env_state(): # act sut.init_env_state(env_state, time) + + @staticmethod + def test_multi_mode(): + # arrange + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + gas_data = ppmc.GasData(GAS_DATA_CTOR_ARG_MINIMAL) + scenario_ctor_arg = SCENARIO_CTOR_ARG_MINIMAL + for entry in ("aero_emissions", "aero_background"): + scenario_ctor_arg[entry][-1]["dist"] = [ + {"A": AERO_MODE_CTOR_LOG_NORMAL["test_mode"]}, + {"B": AERO_MODE_CTOR_LOG_NORMAL["test_mode"]}, + ] + + # act + _ = ppmc.Scenario(gas_data, aero_data, scenario_ctor_arg)