From 406e6047a3fdb016fcaa01b2fcf555ddd973553e Mon Sep 17 00:00:00 2001 From: Jeff Curtis Date: Mon, 17 Nov 2025 17:04:24 -0600 Subject: [PATCH 1/3] add gas state conversions --- src/gas_state.F90 | 25 ++++++++++++++++++++++ src/gas_state.hpp | 15 ++++++++++++- src/pypartmc.cpp | 3 +++ tests/test_gas_state.py | 47 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/gas_state.F90 b/src/gas_state.F90 index 5d501500..3186f515 100644 --- a/src/gas_state.F90 +++ b/src/gas_state.F90 @@ -93,4 +93,29 @@ subroutine f_gas_state_set_size(ptr_c, gas_data_ptr_c) bind(C) call gas_state_set_size(ptr_f, gas_data_n_spec(gas_data_ptr_f)) end subroutine + + subroutine f_gas_state_molar_conc_to_ppb(ptr_c, env_state_ptr_c) bind(C) + type(c_ptr), intent(inout) :: ptr_c + type(c_ptr), intent(in) :: env_state_ptr_c + type(gas_state_t), pointer :: ptr_f => null() + type(env_state_t), pointer :: env_state_ptr_f => null() + + call c_f_pointer(ptr_c, ptr_f) + call c_f_pointer(env_state_ptr_c, env_state_ptr_f) + + call gas_state_molar_conc_to_ppb(ptr_f, env_state_ptr_f) + + end subroutine + + subroutine f_gas_state_scale(ptr_c, alpha) bind(C) + type(c_ptr), intent(inout) :: ptr_c + type(gas_state_t), pointer :: ptr_f => null() + real(kind=c_double), intent(in) :: alpha + + call c_f_pointer(ptr_c, ptr_f) + + call gas_state_scale(ptr_f, alpha) + + end subroutine + end module diff --git a/src/gas_state.hpp b/src/gas_state.hpp index 6b1ad688..eb3d28e2 100644 --- a/src/gas_state.hpp +++ b/src/gas_state.hpp @@ -10,6 +10,7 @@ #include "json_resource.hpp" #include "pmc_resource.hpp" #include "gas_data.hpp" +#include "env_state.hpp" extern "C" void f_gas_state_ctor(void *ptr) noexcept; extern "C" void f_gas_state_dtor(void *ptr) noexcept; @@ -19,7 +20,9 @@ extern "C" void f_gas_state_len(const void *ptr, int *len) noexcept; extern "C" void f_gas_state_to_json(const void *ptr) noexcept; extern "C" void f_gas_state_from_json(const void *ptr, const void *gasdata_ptr) noexcept; extern "C" void f_gas_state_set_size(const void *ptr, const void *gasdata_ptr) noexcept; -extern "C" void f_gas_state_mix_rats(const void *ptr, const double *data, const int *len); +extern "C" void f_gas_state_mix_rats(const void *ptr, const double *data, const int *len) noexcept; +extern "C" void f_gas_state_molar_conc_to_ppb(const void *ptr, const void *envstate_ptr) noexcept; +extern "C" void f_gas_state_scale(const void *ptr_c, const double *alpha) noexcept; struct GasState { PMCResource ptr; @@ -126,4 +129,14 @@ struct GasState { ); guard.check_parameters(); } + + static void molar_conc_to_ppb(const GasState &self, const EnvState &env_state) { + + f_gas_state_molar_conc_to_ppb(self.ptr.f_arg(), env_state.ptr.f_arg()); + } + + static void scale(const GasState &self, const double &alpha) { + + f_gas_state_scale(self.ptr.f_arg(), &alpha); + } }; diff --git a/src/pypartmc.cpp b/src/pypartmc.cpp index 621f8772..2973480e 100644 --- a/src/pypartmc.cpp +++ b/src/pypartmc.cpp @@ -524,6 +524,9 @@ NB_MODULE(_PyPartMC, m) { "Return the mixing ratio of a gas species.") .def_prop_rw("mix_rats", &GasState::mix_rats, &GasState::set_mix_rats, "Provide access (read or write) to the array of mixing ratios.") + .def("molar_conc_to_ppb", &GasState::molar_conc_to_ppb, + "Convert (mol m^{-3}) to (ppb).") + .def("scale", &GasState::scale, "Scale a gas state.") ; nb::class_(m, diff --git a/tests/test_gas_state.py b/tests/test_gas_state.py index 3ab4b495..efe256ae 100644 --- a/tests/test_gas_state.py +++ b/tests/test_gas_state.py @@ -11,7 +11,10 @@ import PyPartMC as ppmc +from .common import ENV_STATE_CTOR_ARG_MINIMAL from .test_gas_data import GAS_DATA_CTOR_ARG_MINIMAL +from .test_aero_data import AERO_DATA_CTOR_ARG_MINIMAL +from .test_scenario import SCENARIO_CTOR_ARG_MINIMAL GAS_DATA_MINIMAL = ppmc.GasData(GAS_DATA_CTOR_ARG_MINIMAL) @@ -103,6 +106,50 @@ def test_get_mix_rats(): # assert assert len(sut.mix_rats) == len(sut) + @staticmethod + def test_scale(): + # arrange + gas_data = ppmc.GasData( + ( + "SO2", + "CO", + ) + ) + sut = ppmc.GasState(gas_data) + gas_state_init_values = ({"SO2": [0.1]}, {"CO": [0.5]}) + sut.mix_rats = gas_state_init_values + + # act + alpha = 5.0 + sut.scale(alpha) + + # assert + idx_set = [] + for item in gas_state_init_values: + keys = item.keys() + assert len(keys) == 1 + key = tuple(keys)[0] + val = tuple(item.values())[0][0] + idx_set.append(gas_data.spec_by_name(key)) + assert sut[gas_data.spec_by_name(key)] == val * alpha + + @staticmethod + def test_molar_conc_to_ppb(): + # arrange + gas_data = GAS_DATA_MINIMAL + aero_data = ppmc.AeroData(AERO_DATA_CTOR_ARG_MINIMAL) + scenario = ppmc.Scenario(gas_data, aero_data, SCENARIO_CTOR_ARG_MINIMAL) + env_state = ppmc.EnvState(ENV_STATE_CTOR_ARG_MINIMAL) + scenario.init_env_state(env_state, 0.0) + sut = ppmc.GasState(GAS_DATA_MINIMAL) + sut.mix_rats = GAS_STATE_MINIMAL + + # act + sut.molar_conc_to_ppb(env_state) + + # assert + assert sut.mix_rats[0] > GAS_STATE_MINIMAL[0][gas_data.species[0]][0] + @staticmethod def test_set_mix_rats_from_json(): # arrange From 78c5f8923c4953635b8922ffbf26492643c55cb8 Mon Sep 17 00:00:00 2001 From: Jeff Curtis Date: Tue, 18 Nov 2025 08:46:11 -0600 Subject: [PATCH 2/3] add gas_state_add --- src/gas_state.F90 | 13 +++++++++++++ src/gas_state.hpp | 6 ++++++ src/pypartmc.cpp | 1 + tests/test_gas_state.py | 17 ++++++++++++++++- 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/gas_state.F90 b/src/gas_state.F90 index 3186f515..24e54536 100644 --- a/src/gas_state.F90 +++ b/src/gas_state.F90 @@ -118,4 +118,17 @@ subroutine f_gas_state_scale(ptr_c, alpha) bind(C) end subroutine + subroutine f_gas_state_add(ptr_c, delta_gas_state_ptr_c) bind(C) + type(c_ptr), intent(inout) :: ptr_c + type(c_ptr), intent(in) :: delta_gas_state_ptr_c + type(gas_state_t), pointer :: ptr_f => null() + type(gas_state_t), pointer :: delta_gas_state_ptr_f => null() + + call c_f_pointer(ptr_c, ptr_f) + call c_f_pointer(delta_gas_state_ptr_c, delta_gas_state_ptr_f) + + call gas_state_add(ptr_f, delta_gas_state_ptr_f) + + end subroutine + end module diff --git a/src/gas_state.hpp b/src/gas_state.hpp index eb3d28e2..c518864c 100644 --- a/src/gas_state.hpp +++ b/src/gas_state.hpp @@ -23,6 +23,7 @@ extern "C" void f_gas_state_set_size(const void *ptr, const void *gasdata_ptr) n extern "C" void f_gas_state_mix_rats(const void *ptr, const double *data, const int *len) noexcept; extern "C" void f_gas_state_molar_conc_to_ppb(const void *ptr, const void *envstate_ptr) noexcept; extern "C" void f_gas_state_scale(const void *ptr_c, const double *alpha) noexcept; +extern "C" void f_gas_state_add(const void *ptr_c, const void *delta_gas_state_ptr_c) noexcept; struct GasState { PMCResource ptr; @@ -139,4 +140,9 @@ struct GasState { f_gas_state_scale(self.ptr.f_arg(), &alpha); } + + static void add(const GasState &self, const GasState &delta_gas_state) { + + f_gas_state_add(self.ptr.f_arg(), delta_gas_state.ptr.f_arg()); + } }; diff --git a/src/pypartmc.cpp b/src/pypartmc.cpp index 2973480e..fb421dbd 100644 --- a/src/pypartmc.cpp +++ b/src/pypartmc.cpp @@ -527,6 +527,7 @@ NB_MODULE(_PyPartMC, m) { .def("molar_conc_to_ppb", &GasState::molar_conc_to_ppb, "Convert (mol m^{-3}) to (ppb).") .def("scale", &GasState::scale, "Scale a gas state.") + .def("add", &GasState::add, "Adds the given gas_state_delta.") ; nb::class_(m, diff --git a/tests/test_gas_state.py b/tests/test_gas_state.py index efe256ae..1d11e211 100644 --- a/tests/test_gas_state.py +++ b/tests/test_gas_state.py @@ -7,13 +7,14 @@ import gc import platform +import numpy as np import pytest import PyPartMC as ppmc from .common import ENV_STATE_CTOR_ARG_MINIMAL -from .test_gas_data import GAS_DATA_CTOR_ARG_MINIMAL from .test_aero_data import AERO_DATA_CTOR_ARG_MINIMAL +from .test_gas_data import GAS_DATA_CTOR_ARG_MINIMAL from .test_scenario import SCENARIO_CTOR_ARG_MINIMAL GAS_DATA_MINIMAL = ppmc.GasData(GAS_DATA_CTOR_ARG_MINIMAL) @@ -150,6 +151,20 @@ def test_molar_conc_to_ppb(): # assert assert sut.mix_rats[0] > GAS_STATE_MINIMAL[0][gas_data.species[0]][0] + @staticmethod + def test_add(): + gas_data = GAS_DATA_MINIMAL + A = ppmc.GasState(GAS_DATA_MINIMAL) + A.mix_rats = GAS_STATE_MINIMAL + B = ppmc.GasState(GAS_DATA_MINIMAL) + B.mix_rats = GAS_STATE_MINIMAL + tot_mix_rats = np.add(A.mix_rats, B.mix_rats) + + # act + A.add(B) + + assert A.mix_rats == tot_mix_rats + @staticmethod def test_set_mix_rats_from_json(): # arrange From 7884630b390f7ee044b9c0089ec710a5cbbe0bbb Mon Sep 17 00:00:00 2001 From: Jeff Curtis Date: Tue, 18 Nov 2025 09:56:42 -0600 Subject: [PATCH 3/3] add gas_state_add_scaled --- src/gas_state.F90 | 14 ++++++++++++++ src/gas_state.hpp | 14 +++++++++++++- src/pypartmc.cpp | 2 ++ tests/test_gas_state.py | 31 ++++++++++++++++++++++--------- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/gas_state.F90 b/src/gas_state.F90 index 24e54536..79c32a8d 100644 --- a/src/gas_state.F90 +++ b/src/gas_state.F90 @@ -131,4 +131,18 @@ subroutine f_gas_state_add(ptr_c, delta_gas_state_ptr_c) bind(C) end subroutine + subroutine f_gas_state_add_scaled(ptr_c, delta_gas_state_ptr_c, alpha) bind(C) + type(c_ptr), intent(inout) :: ptr_c + type(c_ptr), intent(in) :: delta_gas_state_ptr_c + type(gas_state_t), pointer :: ptr_f => null() + type(gas_state_t), pointer :: delta_gas_state_ptr_f => null() + real(kind=c_double), intent(in) :: alpha + + call c_f_pointer(ptr_c, ptr_f) + call c_f_pointer(delta_gas_state_ptr_c, delta_gas_state_ptr_f) + + call gas_state_add_scaled(ptr_f, delta_gas_state_ptr_f, alpha) + + end subroutine + end module diff --git a/src/gas_state.hpp b/src/gas_state.hpp index c518864c..68f5eb2c 100644 --- a/src/gas_state.hpp +++ b/src/gas_state.hpp @@ -23,7 +23,12 @@ extern "C" void f_gas_state_set_size(const void *ptr, const void *gasdata_ptr) n extern "C" void f_gas_state_mix_rats(const void *ptr, const double *data, const int *len) noexcept; extern "C" void f_gas_state_molar_conc_to_ppb(const void *ptr, const void *envstate_ptr) noexcept; extern "C" void f_gas_state_scale(const void *ptr_c, const double *alpha) noexcept; -extern "C" void f_gas_state_add(const void *ptr_c, const void *delta_gas_state_ptr_c) noexcept; +extern "C" void f_gas_state_add(const void *ptr_c, const void *delta_ptr) noexcept; +extern "C" void f_gas_state_add_scaled( + const void *ptr_c, + const void *delta_ptr, + const double *alpha +) noexcept; struct GasState { PMCResource ptr; @@ -145,4 +150,11 @@ struct GasState { f_gas_state_add(self.ptr.f_arg(), delta_gas_state.ptr.f_arg()); } + + static void add_scaled(const GasState &self, const GasState &delta_gas_state, + const double &alpha) { + + f_gas_state_add_scaled(self.ptr.f_arg(), delta_gas_state.ptr.f_arg(), &alpha); + } + }; diff --git a/src/pypartmc.cpp b/src/pypartmc.cpp index fb421dbd..4aa26309 100644 --- a/src/pypartmc.cpp +++ b/src/pypartmc.cpp @@ -528,6 +528,8 @@ NB_MODULE(_PyPartMC, m) { "Convert (mol m^{-3}) to (ppb).") .def("scale", &GasState::scale, "Scale a gas state.") .def("add", &GasState::add, "Adds the given gas_state_delta.") + .def("add_scaled", &GasState::add_scaled, + "Adds the given gas_state_delta scaled by alpha to an existing gas_state.") ; nb::class_(m, diff --git a/tests/test_gas_state.py b/tests/test_gas_state.py index 1d11e211..63e6fede 100644 --- a/tests/test_gas_state.py +++ b/tests/test_gas_state.py @@ -149,21 +149,34 @@ def test_molar_conc_to_ppb(): sut.molar_conc_to_ppb(env_state) # assert - assert sut.mix_rats[0] > GAS_STATE_MINIMAL[0][gas_data.species[0]][0] + assert sut.mix_rats[0] > GAS_STATE_MINIMAL[0][list(gas_data.species)[0]][0] @staticmethod def test_add(): - gas_data = GAS_DATA_MINIMAL - A = ppmc.GasState(GAS_DATA_MINIMAL) - A.mix_rats = GAS_STATE_MINIMAL - B = ppmc.GasState(GAS_DATA_MINIMAL) - B.mix_rats = GAS_STATE_MINIMAL - tot_mix_rats = np.add(A.mix_rats, B.mix_rats) + sut = ppmc.GasState(GAS_DATA_MINIMAL) + sut.mix_rats = GAS_STATE_MINIMAL + gas_state_delta = ppmc.GasState(GAS_DATA_MINIMAL) + gas_state_delta.mix_rats = GAS_STATE_MINIMAL + tot_mix_rats = np.add(sut.mix_rats, gas_state_delta.mix_rats) + + # act + sut.add(gas_state_delta) + + assert sut.mix_rats == tot_mix_rats + + @staticmethod + def test_add_scaled(): + sut = ppmc.GasState(GAS_DATA_MINIMAL) + sut.mix_rats = GAS_STATE_MINIMAL + gas_state_delta = ppmc.GasState(GAS_DATA_MINIMAL) + gas_state_delta.mix_rats = GAS_STATE_MINIMAL + alpha = 2.5 + tot_mix_rats = np.add(sut.mix_rats, np.array(gas_state_delta.mix_rats) * alpha) # act - A.add(B) + sut.add_scaled(gas_state_delta, alpha) - assert A.mix_rats == tot_mix_rats + assert sut.mix_rats == tot_mix_rats @staticmethod def test_set_mix_rats_from_json():