From ede0c3683178d1b54ac706fef41f1f7376244fee Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 13:54:22 -0700 Subject: [PATCH 1/7] add scope setter --- .../process/chemical_reaction_builder.hpp | 82 +++++++++++++++---- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index 423fd5a7b..62df74d4e 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -20,31 +20,65 @@ namespace micm class ChemicalReactionBuilder { - private: - std::vector reactants_; - std::vector products_; - std::unique_ptr rate_constant_; - Phase phase_; - public: - /// @brief Sets the list of reactant species involved in the chemical reaction - /// @param reactants A vector of Species objects representing the reactants - /// @return Reference to the builder + ChemicalReactionBuilder& SetAerosolScope(const std::string& scope, const Phase& phase) + { + scope_ = scope; + phase_ = phase; + has_scope_ = true; + return *this; + } + ChemicalReactionBuilder& SetReactants(std::vector reactants) { - reactants_ = std::move(reactants); + if (has_scope_) + { + for (const auto& species : reactants) + { + reactants_.emplace_back(Scope(species, phase_)); + } + } + else + { + reactants_ = std::move(reactants); + } return *this; } - /// @brief Sets the list of product species and their yields for the chemical reaction - /// @param products A vector of Yield objects representing the products - /// @return Reference to the builder ChemicalReactionBuilder& SetProducts(std::vector products) { - products_ = std::move(products); + if (has_scope_) + { + for (const auto& [species_, coefficient_ ] : products) + { + products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); + } + } + else + { + products_ = std::move(products); + } return *this; } + /// @brief Sets the list of reactant species involved in the chemical reaction + /// @param reactants A vector of Species objects representing the reactants + /// @return Reference to the builder + // ChemicalReactionBuilder& SetReactants(std::vector reactants) + // { + // reactants_ = std::move(reactants); + // return *this; + // } + + // /// @brief Sets the list of product species and their yields for the chemical reaction + // /// @param products A vector of Yield objects representing the products + // /// @return Reference to the builder + // ChemicalReactionBuilder& SetProducts(std::vector products) + // { + // products_ = std::move(products); + // return *this; + // } + /// @brief Sets the rate constant by cloning the provided RateConstant object /// This method performs a deep copy of the given rate constant using its Clone() method. /// Useful when the original rate constant must remain unchanged. @@ -78,6 +112,26 @@ namespace micm ChemicalReaction reaction(std::move(reactants_), std::move(products_), std::move(rate_constant_), phase_); return Process(std::move(reaction)); } + + private: + std::vector reactants_; + std::vector products_; + std::unique_ptr rate_constant_; + Phase phase_; + + bool has_scope_ = false; + std::string scope_; + + /// @brief + /// @param species + /// @param phase + /// @return + Species Scope(const Species& species, const Phase& phase) const + { + Species scoped_species = species; + scoped_species.name_ = scope_ + "." + phase.name_ + "." + species.name_; + return scoped_species; + } }; } // namespace micm \ No newline at end of file From 6857d0bf53b04c6234ec39a2802db4dd9b059aed Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 17:44:52 -0700 Subject: [PATCH 2/7] temp test --- .../process/chemical_reaction_builder.hpp | 72 ++++++++++--------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index 62df74d4e..f1cc75b02 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -28,57 +28,58 @@ namespace micm has_scope_ = true; return *this; } - + + /// @brief Sets the list of reactant species involved in the chemical reaction + /// @param reactants A vector of Species objects representing the reactants + /// @return Reference to the builder ChemicalReactionBuilder& SetReactants(std::vector reactants) { + // if (has_scope_) + // { + // for (auto& species : reactants) + // { + // // reactants_.emplace_back(Scope(species, phase_)); + // Scope(species, phase_); + // } + // } + // else + // { + // reactants_ = std::move(reactants); + // } + // return *this; if (has_scope_) { - for (const auto& species : reactants) + for (auto& species : reactants) { - reactants_.emplace_back(Scope(species, phase_)); + // reactants_.emplace_back(Scope(species, phase_)); + Scope(species, phase_); } } - else - { - reactants_ = std::move(reactants); - } + reactants_ = std::move(reactants); return *this; } + /// @brief Sets the list of product species and their yields for the chemical reaction + /// @param products A vector of Yield objects representing the products + /// @return Reference to the builder ChemicalReactionBuilder& SetProducts(std::vector products) { if (has_scope_) { - for (const auto& [species_, coefficient_ ] : products) + for (auto& [species, coefficient ] : products) { - products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); + // products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); + // products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); + Scope(species, phase_); } } - else - { + // else + // { products_ = std::move(products); - } + // } return *this; } - /// @brief Sets the list of reactant species involved in the chemical reaction - /// @param reactants A vector of Species objects representing the reactants - /// @return Reference to the builder - // ChemicalReactionBuilder& SetReactants(std::vector reactants) - // { - // reactants_ = std::move(reactants); - // return *this; - // } - - // /// @brief Sets the list of product species and their yields for the chemical reaction - // /// @param products A vector of Yield objects representing the products - // /// @return Reference to the builder - // ChemicalReactionBuilder& SetProducts(std::vector products) - // { - // products_ = std::move(products); - // return *this; - // } - /// @brief Sets the rate constant by cloning the provided RateConstant object /// This method performs a deep copy of the given rate constant using its Clone() method. /// Useful when the original rate constant must remain unchanged. @@ -126,11 +127,14 @@ namespace micm /// @param species /// @param phase /// @return - Species Scope(const Species& species, const Phase& phase) const + // Species Scope(const Species& species, const Phase& phase) const + // Species Scope(Species& species, const Phase& phase) + void Scope(Species& species, const Phase& phase) { - Species scoped_species = species; - scoped_species.name_ = scope_ + "." + phase.name_ + "." + species.name_; - return scoped_species; + // Species scoped_species = species; + // scoped_species.name_ = scope_ + "." + phase.name_ + "." + species.name_; + // return scoped_species; + species.name_ = scope_ + "." + phase.name_ + "." + species.name_; } }; From 823337a1f20da6186141bf2bbe38744b304f71b3 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 18:57:05 -0700 Subject: [PATCH 3/7] add aerosol scope test --- .../process/chemical_reaction_builder.hpp | 78 +++++---- test/unit/process/CMakeLists.txt | 1 + test/unit/process/test_aerosol_scope.cpp | 148 ++++++++++++++++++ 3 files changed, 187 insertions(+), 40 deletions(-) create mode 100644 test/unit/process/test_aerosol_scope.cpp diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index f1cc75b02..b1f7cb2cb 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -21,6 +21,12 @@ namespace micm class ChemicalReactionBuilder { public: + /// @brief Enables aerosol scoping for reactant and product species + /// This function must be called before setting reactants or products + /// in order for scoping to be applied. + /// @param scope Aerosol scope prefix to apply to species names + /// @param phase Phase associated with the aerosol scope + /// @return Reference to the builder ChemicalReactionBuilder& SetAerosolScope(const std::string& scope, const Phase& phase) { scope_ = scope; @@ -29,54 +35,52 @@ namespace micm return *this; } - /// @brief Sets the list of reactant species involved in the chemical reaction - /// @param reactants A vector of Species objects representing the reactants + /// @brief Sets the list of reactant species involved in the chemical reaction. + /// When scoping is enabled, each reactant name is prefixed with an aerosol + /// phase–specific scope. + /// @param reactants A list of Species objects representing the reactants /// @return Reference to the builder - ChemicalReactionBuilder& SetReactants(std::vector reactants) + ChemicalReactionBuilder& SetReactants(const std::vector& reactants) { - // if (has_scope_) - // { - // for (auto& species : reactants) - // { - // // reactants_.emplace_back(Scope(species, phase_)); - // Scope(species, phase_); - // } - // } - // else - // { - // reactants_ = std::move(reactants); - // } - // return *this; + reactants_.reserve(reactants.size()); + if (has_scope_) { - for (auto& species : reactants) + for (const auto& species : reactants) { - // reactants_.emplace_back(Scope(species, phase_)); - Scope(species, phase_); + reactants_.push_back(species); + Scope(reactants_.back(), phase_); } } - reactants_ = std::move(reactants); + else + { + reactants_ = reactants; + } + return *this; } - /// @brief Sets the list of product species and their yields for the chemical reaction - /// @param products A vector of Yield objects representing the products + /// @brief Sets the list of product species and their yields for the chemical reaction. + /// When scoping is enabled, each product name is prefixed with an aerosol + /// phase–specific scope. + /// @param products A list of Yield objects representing the products /// @return Reference to the builder - ChemicalReactionBuilder& SetProducts(std::vector products) + ChemicalReactionBuilder& SetProducts(const std::vector& products) { + products_.reserve(products.size()); + if (has_scope_) { - for (auto& [species, coefficient ] : products) + for (const auto& [species, coefficient] : products) { - // products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); - // products_.push_back(Yield{ Scope(species_, phase_), coefficient_ }); - Scope(species, phase_); + products_.emplace_back(species, coefficient); + Scope(products_.back().species_, phase_); } } - // else - // { - products_ = std::move(products); - // } + else + { + products_ = products; + } return *this; } @@ -123,17 +127,11 @@ namespace micm bool has_scope_ = false; std::string scope_; - /// @brief - /// @param species - /// @param phase - /// @return - // Species Scope(const Species& species, const Phase& phase) const - // Species Scope(Species& species, const Phase& phase) + /// @brief Applies an aerosol phase-specific scope to a species by prefixing its name + /// @param species Species object whose name will be modified + /// @param phase Phase whose name is used in the scope prefix void Scope(Species& species, const Phase& phase) { - // Species scoped_species = species; - // scoped_species.name_ = scope_ + "." + phase.name_ + "." + species.name_; - // return scoped_species; species.name_ = scope_ + "." + phase.name_ + "." + species.name_; } }; diff --git a/test/unit/process/CMakeLists.txt b/test/unit/process/CMakeLists.txt index 3e0349b92..59f45de1f 100644 --- a/test/unit/process/CMakeLists.txt +++ b/test/unit/process/CMakeLists.txt @@ -3,6 +3,7 @@ create_standard_test(NAME process SOURCES test_process.cpp) create_standard_test(NAME process_set SOURCES test_process_set.cpp) +create_standard_test(NAME aerosol_scope SOURCES test_aerosol_scope.cpp) add_subdirectory(rate_constant) add_subdirectory(transfer_coefficient) \ No newline at end of file diff --git a/test/unit/process/test_aerosol_scope.cpp b/test/unit/process/test_aerosol_scope.cpp new file mode 100644 index 000000000..efb72c44b --- /dev/null +++ b/test/unit/process/test_aerosol_scope.cpp @@ -0,0 +1,148 @@ +// Copyright (C) 2023-2025 University Corporation for Atmospheric Research +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + +#include + +using namespace micm; + +TEST(ChemicalReactionBuilder, SetAerosolScopeAppliesCorrectScopingToReactants) +{ + // Create species + auto CO2 = Species{ "CO2" }; + auto H2O = Species{ "H2O" }; + + // Create aqueous phase + Phase aqueous_phase{ "aqueous", std::vector{ CO2, H2O } }; + + // Create a reaction with aerosol scoping + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + Process reaction = ChemicalReactionBuilder() + .SetAerosolScope("accumulation", aqueous_phase) + .SetReactants({ CO2 }) + .SetProducts({ Yield(H2O, 1.0) }) + .SetRateConstant(rate_constant) + .Build(); + + // Verify the reaction was created and reactant name was scoped + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + ASSERT_EQ(chem_reaction->reactants_.size(), 1); + EXPECT_EQ(chem_reaction->reactants_[0].name_, "accumulation.aqueous.CO2"); + + // Verify product name was scoped + ASSERT_EQ(chem_reaction->products_.size(), 1); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "accumulation.aqueous.H2O"); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeAppliesCorrectScopingToProducts) +{ + // Create species + auto OH = Species{ "OH-" }; + auto Hplus = Species{ "H+" }; + auto H2O = Species{ "H2O" }; + + // Create aqueous phase + Phase aqueous_phase{ "aqueous", std::vector{ OH, Hplus, H2O } }; + + // Create a reaction with aerosol scoping + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.14e-2 } }; + + Process reaction = ChemicalReactionBuilder() + .SetAerosolScope("aitken", aqueous_phase) + .SetReactants({ H2O }) + .SetProducts({ Yield(OH, 1.0), Yield(Hplus, 1.0) }) + .SetRateConstant(rate_constant) + .Build(); + + // Verify the reaction was created + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + + // Verify reactant name was scoped + ASSERT_EQ(chem_reaction->reactants_.size(), 1); + EXPECT_EQ(chem_reaction->reactants_[0].name_, "aitken.aqueous.H2O"); + + // Verify product names were scoped + ASSERT_EQ(chem_reaction->products_.size(), 2); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "aitken.aqueous.OH-"); + EXPECT_EQ(chem_reaction->products_[1].species_.name_, "aitken.aqueous.H+"); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeWithMultipleReactantsAndProducts) +{ + // Create species + auto A = Species{ "A" }; + auto B = Species{ "B" }; + auto C = Species{ "C" }; + auto D = Species{ "D" }; + + // Create phase + Phase organic_phase{ "organic", std::vector{ A, B, C, D } }; + + // Create a reaction with aerosol scoping + auto rate_constant = ArrheniusRateConstant{ { .A_ = 2.5 } }; + + Process reaction = ChemicalReactionBuilder() + .SetAerosolScope("coarse", organic_phase) + .SetReactants({ A, B }) + .SetProducts({ Yield(C, 1.0), Yield(D, 2.0) }) + .SetRateConstant(rate_constant) + .Build(); + + // Verify the reaction was created + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + + // Verify reactant names were scoped + ASSERT_EQ(chem_reaction->reactants_.size(), 2); + EXPECT_EQ(chem_reaction->reactants_[0].name_, "coarse.organic.A"); + EXPECT_EQ(chem_reaction->reactants_[1].name_, "coarse.organic.B"); + + // Verify product names and yields were preserved + ASSERT_EQ(chem_reaction->products_.size(), 2); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "coarse.organic.C"); + EXPECT_EQ(chem_reaction->products_[0].coefficient_, 1.0); + EXPECT_EQ(chem_reaction->products_[1].species_.name_, "coarse.organic.D"); + EXPECT_EQ(chem_reaction->products_[1].coefficient_, 2.0); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeDifferentPhaseNames) +{ + // Test that different phase names create different scopes + auto species = Species{ "ABC" }; + + Phase phase1{ "aqueous", std::vector{ species } }; + Phase phase2{ "organic", std::vector{ species } }; + + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + // Reaction with phase1 + Process reaction1 = ChemicalReactionBuilder() + .SetAerosolScope("aitken", phase1) + .SetReactants({ species }) + .SetProducts({}) + .SetRateConstant(rate_constant) + .Build(); + + // Reaction with phase2 + Process reaction2 = ChemicalReactionBuilder() + .SetAerosolScope("dust", phase2) + .SetReactants({ species }) + .SetProducts({}) + .SetRateConstant(rate_constant) + .Build(); + + auto* chem_reaction1 = std::get_if(&reaction1.process_); + auto* chem_reaction2 = std::get_if(&reaction2.process_); + + EXPECT_EQ(chem_reaction1->reactants_[0].name_, "aitken.aqueous.ABC"); + EXPECT_EQ(chem_reaction2->reactants_[0].name_, "dust.organic.ABC"); +} \ No newline at end of file From eb71b78b76ba0acce96eb450946353011375afaa Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 21:20:35 -0700 Subject: [PATCH 4/7] add checks for mutually exclusive function --- .../process/chemical_reaction_builder.hpp | 40 +++- include/micm/process/process_error.hpp | 6 +- include/micm/util/error.hpp | 1 + test/unit/process/test_aerosol_scope.cpp | 174 +++++++++++++++++- 4 files changed, 211 insertions(+), 10 deletions(-) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index b1f7cb2cb..2d319e0e5 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -24,11 +24,26 @@ namespace micm /// @brief Enables aerosol scoping for reactant and product species /// This function must be called before setting reactants or products /// in order for scoping to be applied. + /// Cannot be used together with SetPhase - they are mutually exclusive. /// @param scope Aerosol scope prefix to apply to species names /// @param phase Phase associated with the aerosol scope /// @return Reference to the builder + /// @throws std::system_error if SetPhase, SetReactants, or SetProducts has already been called ChemicalReactionBuilder& SetAerosolScope(const std::string& scope, const Phase& phase) { + if (has_phase_) + throw std::system_error( + make_error_code(MicmProcessErrc::InvalidConfiguration), + "SetPhase and SetAerosolScope are mutually exclusive. Do not call both."); + if (has_reactants_) + throw std::system_error( + make_error_code(MicmProcessErrc::InvalidConfiguration), + "SetAerosolScope must be called before SetReactants."); + if (has_products_) + throw std::system_error( + make_error_code(MicmProcessErrc::InvalidConfiguration), + "SetAerosolScope must be called before SetProducts."); + scope_ = scope; phase_ = phase; has_scope_ = true; @@ -37,15 +52,14 @@ namespace micm /// @brief Sets the list of reactant species involved in the chemical reaction. /// When scoping is enabled, each reactant name is prefixed with an aerosol - /// phase–specific scope. + /// phase-specific scope. /// @param reactants A list of Species objects representing the reactants /// @return Reference to the builder ChemicalReactionBuilder& SetReactants(const std::vector& reactants) { - reactants_.reserve(reactants.size()); - if (has_scope_) { + reactants_.reserve(reactants.size()); for (const auto& species : reactants) { reactants_.push_back(species); @@ -57,20 +71,20 @@ namespace micm reactants_ = reactants; } + has_reactants_ = true; return *this; } /// @brief Sets the list of product species and their yields for the chemical reaction. /// When scoping is enabled, each product name is prefixed with an aerosol - /// phase–specific scope. + /// phase-specific scope. /// @param products A list of Yield objects representing the products /// @return Reference to the builder ChemicalReactionBuilder& SetProducts(const std::vector& products) { - products_.reserve(products.size()); - if (has_scope_) { + products_.reserve(products.size()); for (const auto& [species, coefficient] : products) { products_.emplace_back(species, coefficient); @@ -81,6 +95,7 @@ namespace micm { products_ = products; } + has_products_ = true; return *this; } @@ -96,11 +111,19 @@ namespace micm } /// @brief Sets the phase in which the chemical reaction occurs (e.g., gas, aqueous) + /// Cannot be used together with SetAerosolScope - they are mutually exclusive. /// @param phase Phase object representing the reaction phase /// @return Reference to the builder + /// @throws std::system_error if SetAerosolScope has already been called ChemicalReactionBuilder& SetPhase(const Phase& phase) { + if (has_scope_) + throw std::system_error( + make_error_code(MicmProcessErrc::InvalidConfiguration), + "SetPhase and SetAerosolScope are mutually exclusive. Do not call both."); + phase_ = phase; + has_phase_ = true; return *this; } @@ -123,9 +146,12 @@ namespace micm std::vector products_; std::unique_ptr rate_constant_; Phase phase_; + std::string scope_; bool has_scope_ = false; - std::string scope_; + bool has_phase_ = false; + bool has_reactants_ = false; + bool has_products_ = false; /// @brief Applies an aerosol phase-specific scope to a species by prefixing its name /// @param species Species object whose name will be modified diff --git a/include/micm/process/process_error.hpp b/include/micm/process/process_error.hpp index ccb0ef881..b590eba54 100644 --- a/include/micm/process/process_error.hpp +++ b/include/micm/process/process_error.hpp @@ -13,6 +13,7 @@ enum class MicmProcessErrc ProductDoesNotExist = MICM_PROCESS_ERROR_CODE_PRODUCT_DOES_NOT_EXIST, RateConstantIsNotSet = MICM_PROCESS_ERROR_CODE_RATE_CONSTANT_IS_NOT_SET, TransferCoefficientIsNotSet = MICM_PROCESS_ERROR_CODE_TRANSFER_COEFFICIENT_IS_NOT_SET, + InvalidConfiguration = MICM_PROCESS_ERROR_CODE_INVALID_CONFIGURATION, }; namespace std @@ -35,10 +36,11 @@ class MicmProcessErrorCategory : public std::error_category { switch (static_cast(ev)) { - case MicmProcessErrc::RateConstantIsNotSet: return "Rate constant is not set"; - case MicmProcessErrc::TransferCoefficientIsNotSet: return "Transfer coefficient is not set"; case MicmProcessErrc::ReactantDoesNotExist: return "Reactant does not exist"; case MicmProcessErrc::ProductDoesNotExist: return "Product does not exist"; + case MicmProcessErrc::RateConstantIsNotSet: return "Rate constant is not set"; + case MicmProcessErrc::TransferCoefficientIsNotSet: return "Transfer coefficient is not set"; + case MicmProcessErrc::InvalidConfiguration: return "Configuration is not valid"; default: return "Unknown error"; } } diff --git a/include/micm/util/error.hpp b/include/micm/util/error.hpp index dfb961fbf..c5acbc681 100644 --- a/include/micm/util/error.hpp +++ b/include/micm/util/error.hpp @@ -25,6 +25,7 @@ #define MICM_PROCESS_ERROR_CODE_PRODUCT_DOES_NOT_EXIST 2 #define MICM_PROCESS_ERROR_CODE_RATE_CONSTANT_IS_NOT_SET 3 #define MICM_PROCESS_ERROR_CODE_TRANSFER_COEFFICIENT_IS_NOT_SET 4 +#define MICM_PROCESS_ERROR_CODE_INVALID_CONFIGURATION 5 #define MICM_ERROR_CATEGORY_RATE_CONSTANT "MICM Rate Constant" #define MICM_RATE_CONSTANT_ERROR_CODE_MISSING_ARGUMENTS_FOR_SURFACE_RATE_CONSTANT 1 diff --git a/test/unit/process/test_aerosol_scope.cpp b/test/unit/process/test_aerosol_scope.cpp index efb72c44b..b1e34daec 100644 --- a/test/unit/process/test_aerosol_scope.cpp +++ b/test/unit/process/test_aerosol_scope.cpp @@ -145,4 +145,176 @@ TEST(ChemicalReactionBuilder, SetAerosolScopeDifferentPhaseNames) EXPECT_EQ(chem_reaction1->reactants_[0].name_, "aitken.aqueous.ABC"); EXPECT_EQ(chem_reaction2->reactants_[0].name_, "dust.organic.ABC"); -} \ No newline at end of file +} + +// ============================================================================ +// Tests for mutual exclusivity of SetPhase and SetAerosolScope +// ============================================================================ + +TEST(ChemicalReactionBuilder, SetPhaseAfterSetAerosolScopeThrowsError) +{ + auto CO2 = Species{ "CO2" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2 } }; + Phase gas_phase{ "gas", std::vector{ CO2 } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + EXPECT_THROW( + { + ChemicalReactionBuilder() + .SetAerosolScope("accumulation", aqueous_phase) + .SetPhase(gas_phase) // Should throw + .SetReactants({ CO2 }) + .SetProducts({}) + .SetRateConstant(rate_constant) + .Build(); + }, + std::system_error); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeAfterSetPhaseThrowsError) +{ + auto CO2 = Species{ "CO2" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2 } }; + Phase gas_phase{ "gas", std::vector{ CO2 } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + EXPECT_THROW( + { + ChemicalReactionBuilder() + .SetPhase(gas_phase) + .SetAerosolScope("accumulation", aqueous_phase) // Should throw + .SetReactants({ CO2 }) + .SetProducts({}) + .SetRateConstant(rate_constant) + .Build(); + }, + std::system_error); +} + +TEST(ChemicalReactionBuilder, UsingOnlySetPhaseWorks) +{ + auto CO2 = Species{ "CO2" }; + auto H2O = Species{ "H2O" }; + Phase gas_phase{ "gas", std::vector{ CO2, H2O } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + // Should not throw - only SetPhase is used + Process reaction = ChemicalReactionBuilder() + .SetPhase(gas_phase) + .SetReactants({ CO2 }) + .SetProducts({ Yield(H2O, 1.0) }) + .SetRateConstant(rate_constant) + .Build(); + + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + + // Verify no scoping was applied + EXPECT_EQ(chem_reaction->reactants_[0].name_, "CO2"); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "H2O"); +} + +TEST(ChemicalReactionBuilder, UsingOnlySetAerosolScopeWorks) +{ + auto CO2 = Species{ "CO2" }; + auto H2O = Species{ "H2O" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2, H2O } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + // Should not throw - only SetAerosolScope is used + Process reaction = ChemicalReactionBuilder() + .SetAerosolScope("accumulation", aqueous_phase) + .SetReactants({ CO2 }) + .SetProducts({ Yield(H2O, 1.0) }) + .SetRateConstant(rate_constant) + .Build(); + + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + + // Verify scoping was applied + EXPECT_EQ(chem_reaction->reactants_[0].name_, "accumulation.aqueous.CO2"); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "accumulation.aqueous.H2O"); +} + +// ============================================================================ +// Tests for enforcing SetAerosolScope order +// ============================================================================ + +TEST(ChemicalReactionBuilder, SetAerosolScopeAfterSetReactantsThrowsError) +{ + auto CO2 = Species{ "CO2" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2 } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + EXPECT_THROW( + { + ChemicalReactionBuilder() + .SetReactants({ CO2 }) // Called first + .SetAerosolScope("accumulation", aqueous_phase) // Should throw + .SetProducts({}) + .SetRateConstant(rate_constant) + .Build(); + }, + std::system_error); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeAfterSetProductsThrowsError) +{ + auto CO2 = Species{ "CO2" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2 } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + EXPECT_THROW( + { + ChemicalReactionBuilder() + .SetProducts({ Yield(CO2, 1.0) }) // Called first + .SetAerosolScope("accumulation", aqueous_phase) // Should throw + .SetReactants({}) + .SetRateConstant(rate_constant) + .Build(); + }, + std::system_error); +} + +TEST(ChemicalReactionBuilder, SetAerosolScopeAfterBothReactantsAndProductsThrowsError) +{ + auto CO2 = Species{ "CO2" }; + auto H2O = Species{ "H2O" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2, H2O } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + EXPECT_THROW( + { + ChemicalReactionBuilder() + .SetReactants({ CO2 }) + .SetProducts({ Yield(H2O, 1.0) }) + .SetAerosolScope("accumulation", aqueous_phase) // Should throw + .SetRateConstant(rate_constant) + .Build(); + }, + std::system_error); +} + +TEST(ChemicalReactionBuilder, CorrectOrderSetAerosolScopeBeforeReactantsAndProductsWorks) +{ + auto CO2 = Species{ "CO2" }; + auto H2O = Species{ "H2O" }; + Phase aqueous_phase{ "aqueous", std::vector{ CO2, H2O } }; + auto rate_constant = ArrheniusRateConstant{ { .A_ = 1.0 } }; + + // Should not throw - correct order + Process reaction = ChemicalReactionBuilder() + .SetAerosolScope("accumulation", aqueous_phase) // Called first + .SetReactants({ CO2 }) + .SetProducts({ Yield(H2O, 1.0) }) + .SetRateConstant(rate_constant) + .Build(); + + auto* chem_reaction = std::get_if(&reaction.process_); + ASSERT_NE(chem_reaction, nullptr); + + // Verify scoping was applied correctly + EXPECT_EQ(chem_reaction->reactants_[0].name_, "accumulation.aqueous.CO2"); + EXPECT_EQ(chem_reaction->products_[0].species_.name_, "accumulation.aqueous.H2O"); +} From 4e7b3e4760f39451df98129d1092c976a1e0e896 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 21:23:28 -0700 Subject: [PATCH 5/7] new line --- include/micm/process/chemical_reaction_builder.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index 2d319e0e5..b7fc085d3 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -95,6 +95,7 @@ namespace micm { products_ = products; } + has_products_ = true; return *this; } From 3b003968aa8cce3a62ac6a1708f18a4aee027da0 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Mon, 5 Jan 2026 21:51:05 -0700 Subject: [PATCH 6/7] error message --- include/micm/process/chemical_reaction_builder.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index b7fc085d3..0c200fc61 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -34,7 +34,7 @@ namespace micm if (has_phase_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), - "SetPhase and SetAerosolScope are mutually exclusive. Do not call both."); + "SetPhase and SetAerosolScope are mutually exclusiveand and should not be used together"); if (has_reactants_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), @@ -121,7 +121,7 @@ namespace micm if (has_scope_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), - "SetPhase and SetAerosolScope are mutually exclusive. Do not call both."); + "SetPhase and SetAerosolScope are mutually exclusive and should not be used together"); phase_ = phase; has_phase_ = true; From b8b1f00dc1bf44002455f56c4f0eecd995831eeb Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Tue, 6 Jan 2026 11:29:14 -0700 Subject: [PATCH 7/7] code cleanup --- .../process/chemical_reaction_builder.hpp | 21 ++++++++++--------- test/unit/process/CMakeLists.txt | 3 +-- ...ope.cpp => test_process_configuration.cpp} | 0 3 files changed, 12 insertions(+), 12 deletions(-) rename test/unit/process/{test_aerosol_scope.cpp => test_process_configuration.cpp} (100%) diff --git a/include/micm/process/chemical_reaction_builder.hpp b/include/micm/process/chemical_reaction_builder.hpp index 0c200fc61..2d270fdeb 100644 --- a/include/micm/process/chemical_reaction_builder.hpp +++ b/include/micm/process/chemical_reaction_builder.hpp @@ -24,9 +24,9 @@ namespace micm /// @brief Enables aerosol scoping for reactant and product species /// This function must be called before setting reactants or products /// in order for scoping to be applied. - /// Cannot be used together with SetPhase - they are mutually exclusive. + /// Cannot be used together with SetPhase. They are mutually exclusive. /// @param scope Aerosol scope prefix to apply to species names - /// @param phase Phase associated with the aerosol scope + /// @param phase Phase object representing the reaction phase /// @return Reference to the builder /// @throws std::system_error if SetPhase, SetReactants, or SetProducts has already been called ChemicalReactionBuilder& SetAerosolScope(const std::string& scope, const Phase& phase) @@ -34,7 +34,7 @@ namespace micm if (has_phase_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), - "SetPhase and SetAerosolScope are mutually exclusiveand and should not be used together"); + "SetPhase and SetAerosolScope are mutually exclusive and should not be used together."); if (has_reactants_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), @@ -47,12 +47,12 @@ namespace micm scope_ = scope; phase_ = phase; has_scope_ = true; + return *this; } /// @brief Sets the list of reactant species involved in the chemical reaction. - /// When scoping is enabled, each reactant name is prefixed with an aerosol - /// phase-specific scope. + /// When scoping is enabled, each reactant name is prefixed with the preset scope. /// @param reactants A list of Species objects representing the reactants /// @return Reference to the builder ChemicalReactionBuilder& SetReactants(const std::vector& reactants) @@ -72,12 +72,12 @@ namespace micm } has_reactants_ = true; + return *this; } /// @brief Sets the list of product species and their yields for the chemical reaction. - /// When scoping is enabled, each product name is prefixed with an aerosol - /// phase-specific scope. + /// When scoping is enabled, each product name is prefixed with the preset scope. /// @param products A list of Yield objects representing the products /// @return Reference to the builder ChemicalReactionBuilder& SetProducts(const std::vector& products) @@ -97,6 +97,7 @@ namespace micm } has_products_ = true; + return *this; } @@ -112,7 +113,7 @@ namespace micm } /// @brief Sets the phase in which the chemical reaction occurs (e.g., gas, aqueous) - /// Cannot be used together with SetAerosolScope - they are mutually exclusive. + /// Cannot be used together with SetAerosolScope. They are mutually exclusive. /// @param phase Phase object representing the reaction phase /// @return Reference to the builder /// @throws std::system_error if SetAerosolScope has already been called @@ -121,7 +122,7 @@ namespace micm if (has_scope_) throw std::system_error( make_error_code(MicmProcessErrc::InvalidConfiguration), - "SetPhase and SetAerosolScope are mutually exclusive and should not be used together"); + "SetPhase and SetAerosolScope are mutually exclusive and should not be used togethe."); phase_ = phase; has_phase_ = true; @@ -136,7 +137,7 @@ namespace micm { if (!rate_constant_) throw std::system_error( - make_error_code(MicmProcessErrc::RateConstantIsNotSet), "Rate Constant pointer cannot be null"); + make_error_code(MicmProcessErrc::RateConstantIsNotSet), "Rate Constant pointer cannot be null."); ChemicalReaction reaction(std::move(reactants_), std::move(products_), std::move(rate_constant_), phase_); return Process(std::move(reaction)); diff --git a/test/unit/process/CMakeLists.txt b/test/unit/process/CMakeLists.txt index 59f45de1f..4d096e98f 100644 --- a/test/unit/process/CMakeLists.txt +++ b/test/unit/process/CMakeLists.txt @@ -1,9 +1,8 @@ ################################################################################ # Tests - +create_standard_test(NAME aerosol_scope SOURCES test_process_configuration.cpp) create_standard_test(NAME process SOURCES test_process.cpp) create_standard_test(NAME process_set SOURCES test_process_set.cpp) -create_standard_test(NAME aerosol_scope SOURCES test_aerosol_scope.cpp) add_subdirectory(rate_constant) add_subdirectory(transfer_coefficient) \ No newline at end of file diff --git a/test/unit/process/test_aerosol_scope.cpp b/test/unit/process/test_process_configuration.cpp similarity index 100% rename from test/unit/process/test_aerosol_scope.cpp rename to test/unit/process/test_process_configuration.cpp