Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 70 additions & 14 deletions include/micm/process/chemical_reaction_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,67 @@ namespace micm

class ChemicalReactionBuilder
{
private:
std::vector<Species> reactants_;
std::vector<Yield> products_;
std::unique_ptr<RateConstant> 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
/// @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;
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.
/// @param reactants A list of Species objects representing the reactants
/// @return Reference to the builder
ChemicalReactionBuilder& SetReactants(std::vector<Species> reactants)
ChemicalReactionBuilder& SetReactants(const std::vector<Species>& reactants)
{
reactants_ = std::move(reactants);
reactants_.reserve(reactants.size());

if (has_scope_)
{
for (const auto& species : reactants)
{
reactants_.push_back(species);
Scope(reactants_.back(), phase_);
}
}
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<Yield> products)
ChemicalReactionBuilder& SetProducts(const std::vector<Yield>& products)
{
products_ = std::move(products);
products_.reserve(products.size());

if (has_scope_)
{
for (const auto& [species, coefficient] : products)
{
products_.emplace_back(species, coefficient);
Scope(products_.back().species_, phase_);
}
}
else
{
products_ = products;
}
return *this;
}

Expand Down Expand Up @@ -78,6 +117,23 @@ namespace micm
ChemicalReaction reaction(std::move(reactants_), std::move(products_), std::move(rate_constant_), phase_);
return Process(std::move(reaction));
}

private:
std::vector<Species> reactants_;
std::vector<Yield> products_;
std::unique_ptr<RateConstant> rate_constant_;
Phase phase_;

bool has_scope_ = false;
std::string scope_;

/// @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.name_ = scope_ + "." + phase.name_ + "." + species.name_;
}
};

} // namespace micm
1 change: 1 addition & 0 deletions test/unit/process/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
148 changes: 148 additions & 0 deletions test/unit/process/test_aerosol_scope.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (C) 2023-2025 University Corporation for Atmospheric Research
// SPDX-License-Identifier: Apache-2.0

#include <micm/process/chemical_reaction_builder.hpp>
#include <micm/process/process.hpp>
#include <micm/process/rate_constant/arrhenius_rate_constant.hpp>
#include <micm/system/phase.hpp>
#include <micm/system/species.hpp>
#include <micm/system/yield.hpp>

#include <gtest/gtest.h>

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<PhaseSpecies>{ 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<ChemicalReaction>(&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<PhaseSpecies>{ 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<ChemicalReaction>(&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<PhaseSpecies>{ 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<ChemicalReaction>(&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<PhaseSpecies>{ species } };
Phase phase2{ "organic", std::vector<PhaseSpecies>{ 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<ChemicalReaction>(&reaction1.process_);
auto* chem_reaction2 = std::get_if<ChemicalReaction>(&reaction2.process_);

EXPECT_EQ(chem_reaction1->reactants_[0].name_, "aitken.aqueous.ABC");
EXPECT_EQ(chem_reaction2->reactants_[0].name_, "dust.organic.ABC");
}
Loading