Skip to content
Merged
115 changes: 100 additions & 15 deletions include/micm/process/chemical_reaction_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <micm/system/phase.hpp>
#include <micm/system/species.hpp>
#include <micm/system/yield.hpp>
#include <micm/util/utils.hpp>

#include <memory>
#include <utility>
Expand All @@ -20,28 +21,84 @@ 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.
/// Cannot be used together with SetPhase. They are mutually exclusive.
/// @param scope Aerosol scope prefix to apply to species names
/// @param phase Phase object representing the reaction phase
/// @return Reference to the builder
ChemicalReactionBuilder& SetReactants(std::vector<Species> reactants)
/// @throws std::system_error if SetPhase, SetReactants, or SetProducts has already been called
ChemicalReactionBuilder& SetAerosolScope(const std::string& scope, const Phase& phase)
{
reactants_ = std::move(reactants);
if (has_phase_)
throw std::system_error(
make_error_code(MicmProcessErrc::InvalidConfiguration),
"SetPhase and SetAerosolScope are mutually exclusive and should not be used together.");
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;

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 reactant species involved in the chemical reaction.
/// 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& SetProducts(std::vector<Yield> products)
ChemicalReactionBuilder& SetReactants(const std::vector<Species>& reactants)
{
products_ = std::move(products);
if (has_scope_)
{
reactants_.reserve(reactants.size());
for (const auto& species : reactants)
{
reactants_.push_back(species);
Scope(reactants_.back(), phase_);
}
}
else
{
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 the preset scope.
/// @param products A list of Yield objects representing the products
/// @return Reference to the builder
ChemicalReactionBuilder& SetProducts(const std::vector<Yield>& products)
{
if (has_scope_)
{
products_.reserve(products.size());
for (const auto& [species, coefficient] : products)
{
products_.emplace_back(species, coefficient);
Scope(products_.back().species_, phase_);
}
}
else
{
products_ = products;
}

has_products_ = true;

return *this;
}

Expand All @@ -57,11 +114,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 and should not be used togethe.");

phase_ = phase;
has_phase_ = true;
return *this;
}

Expand All @@ -73,11 +138,31 @@ 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));
}

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

bool has_scope_ = false;
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
/// @param phase Phase whose name is used in the scope prefix
void Scope(Species& species, const Phase& phase)
{
species.name_ = JoinStrings({scope_, phase.name_, species.name_});
}
};

} // namespace micm
6 changes: 4 additions & 2 deletions include/micm/process/process_error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,10 +36,11 @@ class MicmProcessErrorCategory : public std::error_category
{
switch (static_cast<MicmProcessErrc>(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";
}
}
Expand Down
54 changes: 31 additions & 23 deletions include/micm/system/system.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

#include <micm/system/phase.hpp>
#include <micm/system/species.hpp>
#include <micm/util/utils.hpp>

#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace micm
Expand All @@ -16,7 +18,7 @@ namespace micm
{
Phase gas_phase_{};
std::unordered_map<std::string, Phase> phases_{};
std::unordered_map<std::string, std::string> others_{};
std::vector<std::string> others_{};
};

/// @brief Represents the complete chemical state of a grid cell
Expand All @@ -28,9 +30,8 @@ namespace micm
Phase gas_phase_;
/// @brief Additional phases (e.g., aqueous, aerosol), mapped by name and representing non-gas phase
std::unordered_map<std::string, Phase> phases_;
/// @brief Tracks non-phase elements (e.g., number concentrations) associated with a model.
/// Elements are mapped using a prefix specific to the model's name and representation.
std::unordered_map<std::string, std::string> others_;
/// @brief Tracks non-phase elements (e.g., number concentrations) associated with a model
std::vector<std::string> others_;

/// @brief Default constructor
System() = default;
Expand All @@ -39,7 +40,7 @@ namespace micm
System(
const Phase& gas_phase,
const std::unordered_map<std::string, Phase>& phases,
const std::unordered_map<std::string, std::string>& others)
const std::vector<std::string>& others)
: gas_phase_(gas_phase),
phases_(phases),
others_(others)
Expand All @@ -58,7 +59,7 @@ namespace micm
System(
Phase&& gas_phase,
std::unordered_map<std::string, Phase>&& phases,
const std::unordered_map<std::string, std::string>& others)
const std::vector<std::string>& others)
: gas_phase_(std::move(gas_phase)),
phases_(std::move(phases)),
others_(std::move(others))
Expand Down Expand Up @@ -109,12 +110,9 @@ namespace micm

inline size_t System::StateSize() const
{
size_t state_size = gas_phase_.StateSize();
for (const auto& phase : phases_)
{
state_size += phase.second.StateSize();
}
state_size += others_.size();
std::size_t state_size = gas_phase_.StateSize() + others_.size();
for (const auto& [key, phase] : phases_)
state_size += phase.StateSize();

return state_size;
}
Expand All @@ -127,22 +125,32 @@ namespace micm
inline std::vector<std::string> System::UniqueNames(
const std::function<std::string(const std::vector<std::string>& variables, const std::size_t i)> f) const
{
std::vector<std::string> names = gas_phase_.UniqueNames();
for (const auto& phase : phases_)
{
for (const auto& species_name : phase.second.UniqueNames())
names.push_back(phase.first + "." + species_name);
}
for (const auto& other : others_)
std::vector<std::string> names;
names.reserve(StateSize());

auto gas_names = gas_phase_.UniqueNames();
names.insert(names.end(),
std::make_move_iterator(gas_names.begin()),
std::make_move_iterator(gas_names.end()));

for (const auto& [key, phase] : phases_)
{
names.push_back(other.first + "." + other.second);
auto phase_names = phase.UniqueNames();
for (auto& species_name : phase_names)
names.push_back(JoinStrings({key, std::move(species_name)}));
}

names.insert(names.end(), others_.begin(), others_.end());

if (f)
{
const auto orig_names = names;
for (std::size_t i = 0; i < orig_names.size(); ++i)
names[i] = f(orig_names, i);
std::vector<std::string> reordered;
reordered.reserve(names.size());
for (std::size_t i = 0; i < names.size(); ++i)
reordered.push_back(f(names, i));
return reordered;
}

return names;
}

Expand Down
1 change: 1 addition & 0 deletions include/micm/util/error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions include/micm/util/utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (C) 2023-2025 University Corporation for Atmospheric Research
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <string>
#include <vector>

namespace micm
{
inline std::string JoinStrings(const std::vector<std::string>& names)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do this with std::fmt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it but It looks like std::format_to isn’t available with the GCC/libstdc++ versions we currently support. Should we revert the change or trying updating a newer GCC?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I guess revert the change. Maybe sometime later this year we can adopt support for std::fmt

{
std::string result;
for (size_t i = 0; i < names.size(); ++i)
{
if (!names[i].empty())
{
if (!result.empty())
result += ".";
result += names[i];
}
}
return result;
}

} // namespace micm
2 changes: 1 addition & 1 deletion test/unit/process/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
################################################################################
# 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)

Expand Down
Loading
Loading