Skip to content

Commit d6dbe31

Browse files
slayoojcurtis2
andauthored
support for AeroMode instantiation with sampled mode type (incl. fixes in spec file logic) + sampled and mono mode types coverage in unit tests (in test_aero_[mode,state,dist].py) (#357)
Co-authored-by: Jeffrey Curtis <[email protected]>
1 parent 0c8eec6 commit d6dbe31

9 files changed

+316
-10
lines changed

src/aero_mode.F90

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,40 @@ subroutine f_aero_mode_get_name(ptr_c, name_data, name_size) bind(C)
244244
name_data = c_loc(aero_mode%name)
245245
name_size = len_trim(aero_mode%name)
246246
end subroutine
247+
248+
subroutine f_aero_mode_get_sample_num_conc(ptr_c, arr_data, data_size) bind(C)
249+
type(c_ptr), intent(in) :: ptr_c
250+
type(aero_mode_t), pointer :: aero_mode => null()
251+
integer(c_int), intent(in) :: data_size
252+
real(c_double), dimension(data_size), intent(inout) :: arr_data
253+
254+
call c_f_pointer(ptr_c, aero_mode)
255+
256+
arr_data = aero_mode%sample_num_conc
257+
258+
end subroutine
259+
260+
subroutine f_aero_mode_get_sample_radius(ptr_c, arr_data, data_size) bind(C)
261+
type(c_ptr), intent(in) :: ptr_c
262+
type(aero_mode_t), pointer :: aero_mode => null()
263+
integer(c_int), intent(in) :: data_size
264+
real(c_double), dimension(data_size), intent(inout) :: arr_data
265+
266+
call c_f_pointer(ptr_c, aero_mode)
267+
268+
arr_data = aero_mode%sample_radius
269+
270+
end subroutine
271+
272+
subroutine f_aero_mode_get_sample_bins(ptr_c, n_bins) bind(c)
273+
type(c_ptr), intent(in) :: ptr_c
274+
type(aero_mode_t), pointer :: aero_mode => null()
275+
integer(c_int), intent(out) :: n_bins
276+
277+
call c_f_pointer(ptr_c, aero_mode)
278+
279+
n_bins = size(aero_mode%sample_num_conc)
280+
281+
end subroutine
282+
247283
end module

src/aero_mode.hpp

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ extern "C" void f_aero_mode_from_json(
118118
void *aero_data_ptr
119119
) noexcept;
120120

121+
extern "C" void f_aero_mode_get_sample_num_conc(
122+
const void *ptr,
123+
void *sample_num_conc_data,
124+
const int *sample_num_conc_data_size
125+
) noexcept;
126+
127+
extern "C" void f_aero_mode_get_sample_radius(
128+
const void *ptr,
129+
void *sample_radius_data,
130+
const int *sample_radius_data_size
131+
) noexcept;
132+
133+
extern "C" void f_aero_mode_get_sample_bins(const void *ptr, int *n_bins) noexcept;
134+
121135
struct AeroMode {
122136
PMCResource ptr;
123137

@@ -145,6 +159,24 @@ struct AeroMode {
145159
throw std::runtime_error("mass_frac value must be a list of single-element dicts");
146160
if (!InputJSONResource::unique_keys(mass_frac))
147161
throw std::runtime_error("mass_frac keys must be unique");
162+
if (mode["mode_type"] == "sampled") {
163+
if (mode.find("size_dist") == mode.end())
164+
throw std::runtime_error("size_dist key must be set for mode_type=sampled");
165+
auto sd = mode["size_dist"];
166+
if (
167+
sd.size() != 2 ||
168+
!sd[0].is_object() ||
169+
sd[0].size() != 1 ||
170+
sd[1].size() != 1 ||
171+
sd[0].find("diam") == sd[0].end() ||
172+
sd[1].find("num_conc") == sd[1].end()
173+
)
174+
throw std::runtime_error("size_dist value must be an iterable of two single-element dicts (first with 'diam', second with 'num_conc' as keys)");
175+
auto diam = *sd[0].find("diam");
176+
auto num_conc = *sd[1].find("num_conc");
177+
if (diam.size() != num_conc.size() + 1)
178+
throw std::runtime_error("size_dist['num_conc'] must have len(size_dist['diam'])-1 elements");
179+
}
148180
}
149181

150182
static auto get_num_conc(const AeroMode &self){
@@ -291,7 +323,7 @@ struct AeroMode {
291323
int type;
292324
f_aero_mode_get_type(self.ptr.f_arg(), &type);
293325

294-
if (type < 0 || (unsigned int)type >= AeroMode::types().size())
326+
if (type <= 0 || (unsigned int)type > AeroMode::types().size())
295327
throw std::logic_error("Unknown mode type.");
296328

297329
return AeroMode::types()[type - 1];
@@ -312,4 +344,29 @@ struct AeroMode {
312344
name[i] = f_ptr[i];
313345
return name;
314346
}
347+
348+
static auto get_sample_radius(const AeroMode &self) {
349+
int len;
350+
f_aero_mode_get_sample_bins(self.ptr.f_arg(), &len);
351+
len++;
352+
std::valarray<double> sample_radius(len);
353+
f_aero_mode_get_sample_radius(
354+
self.ptr.f_arg(),
355+
begin(sample_radius),
356+
&len
357+
);
358+
return sample_radius;
359+
}
360+
361+
static auto get_sample_num_conc(const AeroMode &self) {
362+
int len;
363+
f_aero_mode_get_sample_bins(self.ptr.f_arg(), &len);
364+
std::valarray<double> sample_num_conc(len);
365+
f_aero_mode_get_sample_num_conc(
366+
self.ptr.f_arg(),
367+
begin(sample_num_conc),
368+
&len
369+
);
370+
return sample_num_conc;
371+
}
315372
};

src/json_resource.hpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct JSONResource {
2626
}
2727

2828
protected:
29-
size_t index = 0;
29+
size_t index = 0, named_array_read_count = 0;
3030

3131
JSONResource() {}
3232

@@ -71,6 +71,10 @@ struct JSONResource {
7171
public:
7272
virtual ~JSONResource() {}
7373

74+
auto n_named_array_read_count() noexcept {
75+
return this->named_array_read_count;
76+
}
77+
7478
void zoom_in(const bpstd::string_view &sub) noexcept {
7579
auto it = this->json->is_array()
7680
? this->json->at(this->json->size()-1).find(sub)
@@ -84,6 +88,7 @@ struct JSONResource {
8488
else
8589
this->set_current_json_ptr(&(*it));
8690

91+
this->named_array_read_count = 0;
8792
}
8893

8994
void zoom_out() noexcept {
@@ -101,13 +106,12 @@ struct JSONResource {
101106
return this->json->begin();
102107
}
103108

104-
// TODO #112: to be removed after initialising GasData with a list, and not JSON?
105-
auto first_field_name() const noexcept {
109+
auto first_field_name() noexcept {
106110
// TODO #112: handle errors
107111
std::string name = "";
108112
assert(this->json->size() > 0);
109113
assert(this->json->begin()->size() > 0);
110-
for (auto &entry : this->json->at(0).items())
114+
for (auto &entry : this->json->at(this->named_array_read_count++).items())
111115
{
112116
name = entry.key();
113117
}

src/pypartmc.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,10 @@ PYBIND11_MODULE(_PyPartMC, m) {
453453
.def_property("gsd", &AeroMode::get_gsd,
454454
&AeroMode::set_gsd, "Geometric standard deviation")
455455
.def("set_sample", &AeroMode::set_sampled)
456+
.def_property_readonly("sample_num_conc", &AeroMode::get_sample_num_conc,
457+
"Sample bin number concentrations (m^{-3})")
458+
.def_property_readonly("sample_radius", &AeroMode::get_sample_radius,
459+
"Sample bin radii (m).")
456460
.def_property("type", &AeroMode::get_type, &AeroMode::set_type,
457461
"Mode type (given by module constants)")
458462
.def_property("name", &AeroMode::get_name, &AeroMode::set_name,

src/spec_file_pypartmc.F90

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ subroutine spec_file_read_real_named_array(file, max_lines, names, vals)
129129
n_rows = max_lines
130130
end if
131131

132+
if (allocated(names)) deallocate(names)
133+
if (allocated(vals)) deallocate(vals)
132134
allocate(names(n_rows))
133135
allocate(vals(n_rows, n_cols))
134136
allocate(vals_row(max(1, n_cols)))

src/spec_file_pypartmc.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ void spec_file_read_real_named_array_size(
145145
int *n_rows,
146146
int *n_cols
147147
) noexcept {
148-
auto first_field = json_resource_ptr()->first_field_name();
149148
*n_rows = json_resource_ptr()->n_numeric_array_entries();
149+
150+
auto first_field = json_resource_ptr()->first_field_name();
150151
*n_cols = json_resource_ptr()->n_elements(first_field);
151-
// TODO #112: check each line has the same number of elements as time
152152
}
153153

154154
extern "C"
@@ -174,17 +174,17 @@ void spec_file_read_real_named_array_data(
174174
++i, ++it
175175
) {
176176
assert(it->is_object());
177-
if (i == row-1) {
177+
if (i == (row - 1) + (json_resource_ptr()->n_named_array_read_count() - 1)) {
178178
assert(it->size() == 1);
179179
for (auto &entry : it->items()) {
180-
// TODO #112: use input name_size as limit param
180+
assert(*name_size > (long)entry.key().size());
181181
for (auto c=0u; c < entry.key().size(); ++c)
182182
name_data[c] = entry.key()[c];
183183
*name_size = entry.key().size();
184184
for (auto idx=0u; idx < entry.value().size(); ++idx) {
185185
vals[idx] = entry.value().at(idx).get<double>();
186186
}
187-
break; // TODO #112
187+
break;
188188
}
189189
}
190190
}

tests/test_aero_dist.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,22 @@ def test_ctor_error_on_repeated_massfrac_keys():
217217

218218
# assert
219219
assert str(exc_info.value) == "mass_frac keys must be unique"
220+
221+
@staticmethod
222+
def test_ctor_sampled_mode():
223+
# arrange
224+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
225+
ctor_arg = copy.deepcopy(AERO_DIST_CTOR_ARG_MINIMAL)
226+
ctor_arg[0]["test_mode"]["mode_type"] = "sampled"
227+
ctor_arg[0]["test_mode"]["size_dist"] = [
228+
{"diam": [1, 2, 3, 4]},
229+
{"num_conc": [1, 2, 3]},
230+
]
231+
232+
# act
233+
sut = ppmc.AeroDist(aero_data, ctor_arg)
234+
235+
# assert
236+
assert sut.mode(0).num_conc == sum(
237+
ctor_arg[0]["test_mode"]["size_dist"][1]["num_conc"]
238+
)

tests/test_aero_mode.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@
4848
}
4949
}
5050

51+
AERO_MODE_CTOR_SAMPLED = {
52+
"test_mode": {
53+
"mass_frac": [{"H2O": [1]}],
54+
"diam_type": "geometric",
55+
"mode_type": "sampled",
56+
"size_dist": [
57+
{"diam": [1, 2, 3, 4]},
58+
{"num_conc": [100, 200, 300]},
59+
],
60+
}
61+
}
62+
5163

5264
class TestAeroMode:
5365
@staticmethod
@@ -289,3 +301,127 @@ def test_segfault_case(): # TODO #319
289301
)
290302
print(fishy_ctor_arg)
291303
ppmc.AeroMode(aero_data, fishy_ctor_arg)
304+
305+
@staticmethod
306+
@pytest.mark.skipif(platform.machine() == "arm64", reason="TODO #348")
307+
def test_sampled_without_size_dist():
308+
# arrange
309+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
310+
fishy_ctor_arg = copy.deepcopy(AERO_MODE_CTOR_LOG_NORMAL)
311+
fishy_ctor_arg["test_mode"]["mode_type"] = "sampled"
312+
313+
# act
314+
with pytest.raises(Exception) as exc_info:
315+
ppmc.AeroMode(aero_data, fishy_ctor_arg)
316+
317+
# assert
318+
assert str(exc_info.value) == "size_dist key must be set for mode_type=sampled"
319+
320+
@staticmethod
321+
@pytest.mark.parametrize(
322+
"fishy",
323+
(
324+
None,
325+
[],
326+
[{}, {}, {}],
327+
[{}, []],
328+
[{"diam": None}, {}],
329+
[{"num_conc": None}, {}],
330+
[{"diam": None, "": None}, {}],
331+
[{"num_conc": None, "": None}, {}],
332+
),
333+
)
334+
@pytest.mark.skipif(platform.machine() == "arm64", reason="TODO #348")
335+
def test_sampled_with_fishy_size_dist(fishy):
336+
# arrange
337+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
338+
fishy_ctor_arg = copy.deepcopy(AERO_MODE_CTOR_LOG_NORMAL)
339+
fishy_ctor_arg["test_mode"]["mode_type"] = "sampled"
340+
fishy_ctor_arg["test_mode"]["size_dist"] = fishy
341+
342+
# act
343+
with pytest.raises(Exception) as exc_info:
344+
ppmc.AeroMode(aero_data, fishy_ctor_arg)
345+
346+
# assert
347+
assert (
348+
str(exc_info.value)
349+
== "size_dist value must be an iterable of two single-element dicts"
350+
+ " (first with 'diam', second with 'num_conc' as keys)"
351+
)
352+
353+
@staticmethod
354+
@pytest.mark.skipif(platform.machine() == "arm64", reason="TODO #348")
355+
def test_sampled_with_diam_of_different_len_than_num_conc():
356+
# arrange
357+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
358+
fishy_ctor_arg = copy.deepcopy(AERO_MODE_CTOR_LOG_NORMAL)
359+
fishy_ctor_arg["test_mode"]["mode_type"] = "sampled"
360+
fishy_ctor_arg["test_mode"]["size_dist"] = [
361+
{"diam": [1, 2, 3]},
362+
{"num_conc": [1, 2, 3]},
363+
]
364+
365+
# act
366+
with pytest.raises(Exception) as exc_info:
367+
ppmc.AeroMode(aero_data, fishy_ctor_arg)
368+
369+
# assert
370+
assert (
371+
str(exc_info.value)
372+
== "size_dist['num_conc'] must have len(size_dist['diam'])-1 elements"
373+
)
374+
375+
@staticmethod
376+
def test_sampled():
377+
# arrange
378+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
379+
380+
# act
381+
sut = ppmc.AeroMode(aero_data, AERO_MODE_CTOR_SAMPLED)
382+
383+
# assert
384+
assert sut.type == "sampled"
385+
assert sut.num_conc == np.sum(
386+
AERO_MODE_CTOR_SAMPLED["test_mode"]["size_dist"][1]["num_conc"]
387+
)
388+
assert (
389+
sut.sample_num_conc
390+
== AERO_MODE_CTOR_SAMPLED["test_mode"]["size_dist"][1]["num_conc"]
391+
)
392+
assert (
393+
np.array(sut.sample_radius) * 2
394+
== AERO_MODE_CTOR_SAMPLED["test_mode"]["size_dist"][0]["diam"]
395+
).all()
396+
397+
@staticmethod
398+
def test_set_sample():
399+
# arrange
400+
aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL)
401+
402+
diams = [1, 2, 3, 4]
403+
num_concs = [100, 200, 300]
404+
sut = ppmc.AeroMode(
405+
aero_data,
406+
{
407+
"test_mode": {
408+
"mass_frac": [{"H2O": [1]}],
409+
"diam_type": "geometric",
410+
"mode_type": "sampled",
411+
"size_dist": [
412+
{"diam": diams},
413+
{"num_conc": num_concs},
414+
],
415+
}
416+
},
417+
)
418+
num_conc_orig = sut.num_conc
419+
# act
420+
diams = [0.5 * x for x in diams]
421+
num_concs = [2 * x for x in num_concs]
422+
sut.set_sample(diams, num_concs)
423+
# assert
424+
assert sut.num_conc == np.sum(num_concs)
425+
assert sut.sample_num_conc == num_concs
426+
assert (np.array(sut.sample_radius) * 2 == diams).all()
427+
assert sut.num_conc == num_conc_orig * 2

0 commit comments

Comments
 (0)