diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 9e9bc0e..d3160bb 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -7,38 +7,13 @@ concurrency: cancel-in-progress: true jobs: - xcode_macos_12: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name - runs-on: macos-12 - strategy: - matrix: - # all available versions of xcode: https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#xcode - xcode: ['13.1', '14.1'] - build_type: [Debug, Release] - env: - DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer - - steps: - - uses: actions/checkout@v3 - - - name: Run Cmake - run: cmake -S . -B build -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} - - - name: Build - run: cmake --build build --parallel 10 - - - name: Run tests - run: | - cd build - ctest -C ${{ matrix.build_type }} --rerun-failed --output-on-failure . --verbose -j 10 - xcode_macos_13: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name runs-on: macos-13 strategy: matrix: # all available versions of xcode: https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode - xcode: ['14.1', '15.0'] + xcode: ['15.0'] build_type: [Debug, Release] env: DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer @@ -63,7 +38,6 @@ jobs: strategy: matrix: compiler: - - { cpp: g++-11, c: gcc-11} - { cpp: g++-12, c: gcc-12} - { cpp: clang++, c: clang} build_type: [Debug, Release] diff --git a/examples/full_configuration.json b/examples/full_configuration.json index 704362e..c411c92 100644 --- a/examples/full_configuration.json +++ b/examples/full_configuration.json @@ -76,7 +76,10 @@ "species": [ "H2O2_aq", "H2O_aq", - "ethanol_aq" + "ethanol_aq", + "A", + "B", + "C" ] }, { @@ -120,13 +123,11 @@ }, { "type": "AQUEOUS_EQUILIBRIUM", - "gas phase": "gas", "aerosol phase": "aqueous aerosol", "aerosol-phase water": "H2O_aq", "A": 1.14e-2, "C": 2300.0, "k_reverse": 0.32, - "ion pair": "B-C", "reactants": [ { "species name": "A", diff --git a/include/open_atmos/mechanism_configuration/parser.hpp b/include/open_atmos/mechanism_configuration/parser.hpp index aba0668..86e2a80 100644 --- a/include/open_atmos/mechanism_configuration/parser.hpp +++ b/include/open_atmos/mechanism_configuration/parser.hpp @@ -35,7 +35,8 @@ namespace open_atmos ReactionRequiresUnknownSpecies, UnknownPhase, RequestedAerosolSpeciesNotIncludedInAerosolPhase, - TooManyReactionComponents + TooManyReactionComponents, + InvalidIonPair }; std::string configParseStatusToString(const ConfigParseStatus &status); diff --git a/include/open_atmos/mechanism_configuration/validation.hpp b/include/open_atmos/mechanism_configuration/validation.hpp index 17be6e9..e547545 100644 --- a/include/open_atmos/mechanism_configuration/validation.hpp +++ b/include/open_atmos/mechanism_configuration/validation.hpp @@ -116,6 +116,15 @@ namespace open_atmos const std::string FirstOrderLoss_key = "FIRST_ORDER_LOSS"; // also scaling factor + // Aqueous Equilibrium + const std::string AqueousPhaseEquilibrium_key = "AQUEOUS_EQUILIBRIUM"; + // also + // aerosol phase + // aerosol-phase water + // A + // C + const std::string k_reverse = "k_reverse"; + // Wet Deposition const std::string WetDeposition_key = "WET_DEPOSITION"; // also @@ -133,11 +142,11 @@ namespace open_atmos } keys; - struct Configuration + struct Mechanism { const std::vector required_keys{ keys.version, keys.species, keys.phases, keys.reactions }; const std::vector optional_keys{ keys.name }; - } configuration; + } mechanism; struct Species { @@ -237,11 +246,10 @@ namespace open_atmos const std::vector optional_keys{ keys.name }; } henrys_law; - struct Mechanism + struct AqueousEquilibrium { - const std::vector required_keys{}; - const std::vector optional_keys{}; - } mechanism; - + const std::vector required_keys{ keys.type, keys.reactants, keys.products, keys.aerosol_phase, keys.aerosol_phase_water, keys.k_reverse }; + const std::vector optional_keys{ keys.name, keys.A, keys.C }; + } aqueous_equilibrium; } // namespace validation } // namespace open_atmos \ No newline at end of file diff --git a/include/open_atmos/types.hpp b/include/open_atmos/types.hpp index 63bd640..9a7fac5 100644 --- a/include/open_atmos/types.hpp +++ b/include/open_atmos/types.hpp @@ -6,6 +6,10 @@ #include #include +#include +#include +#include + namespace open_atmos { @@ -240,6 +244,29 @@ namespace open_atmos /// @brief Unknown properties, prefixed with two underscores (__) std::unordered_map unknown_properties; }; + struct AqueousEquilibrium + { + /// @brief An identifier, optional, uniqueness not enforced + std::string name; + /// @brief An identifier indicating which gas phase this reaction takes place in + std::string gas_phase; + /// @brief An identifier indicating which aerosol phase this reaction takes place in + std::string aerosol_phase; + /// @brief An identifier indicating the species label of aqueous phase water + std::string aerosol_phase_water; + /// @brief A list of reactants + std::vector reactants; + /// @brief A list of products + std::vector products; + /// @brief Pre-exponential factor (s-1) + double A{ 1 }; + /// @brief A constant + double C{ 0 }; + /// @brief Reverse reation rate constant (s-1) + double k_reverse{ 0 }; + /// @brief Unknown properties, prefixed with two underscores (__) + std::unordered_map unknown_properties; + }; struct WetDeposition { @@ -279,6 +306,7 @@ namespace open_atmos std::vector condensed_phase_photolysis; std::vector emission; std::vector first_order_loss; + std::vector aqueous_equilibrium; std::vector wet_deposition; std::vector henrys_law; std::vector photolysis; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 861f1b9..7d6e7e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,7 +5,7 @@ configure_file(version.hpp.in ${PROJECT_SOURCE_DIR}/include/open_atmos/mechanism add_library(mechanism_configuration) add_library(open_atmos::mechanism_configuration ALIAS mechanism_configuration) -target_compile_features(mechanism_configuration PUBLIC cxx_std_20) +target_compile_features(mechanism_configuration PUBLIC cxx_std_17) target_sources(mechanism_configuration PRIVATE diff --git a/src/parser.cpp b/src/parser.cpp index d84a839..b1d1308 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -32,6 +32,7 @@ namespace open_atmos case ConfigParseStatus::UnknownPhase: return "UnknownPhase"; case ConfigParseStatus::RequestedAerosolSpeciesNotIncludedInAerosolPhase: return "RequestedAerosolSpeciesNotIncludedInAerosolPhase"; case ConfigParseStatus::TooManyReactionComponents: return "TooManyReactionComponents"; + case ConfigParseStatus::InvalidIonPair: return "InvalidIonPair"; default: return "Unknown"; } } @@ -137,7 +138,7 @@ namespace open_atmos // now, anything left must be standard comment starting with __ for (auto& key : remaining) { - if (!key.starts_with("__")) + if (key.find("__") == std::string::npos) { std::cerr << "Non-standard key '" << key << "' found in object" << object << std::endl; @@ -1332,6 +1333,117 @@ namespace open_atmos return { status, first_order_loss }; } + /// @brief Parses an aqueous equilibrium reaction + /// @param object A json object that should have information containing arrhenius parameters + /// @param existing_species A list of species configured in a mechanism + /// @param existing_phases A list of phases configured in a mechanism + /// @return A pair indicating parsing success and a struct of Condensed Phase Arrhenius parameters + std::pair ParseAqueousEquilibrium( + const json& object, + const std::vector& existing_species, + const std::vector& existing_phases) + { + ConfigParseStatus status = ConfigParseStatus::Success; + types::AqueousEquilibrium aqueous_equilibrium; + + status = ValidateSchema(object, validation::aqueous_equilibrium.required_keys, validation::aqueous_equilibrium.optional_keys); + if (status == ConfigParseStatus::Success) + { + std::vector products{}; + for (const auto& product : object[validation::keys.products]) + { + auto product_parse = ParseReactionComponent(product); + status = product_parse.first; + if (status != ConfigParseStatus::Success) + { + break; + } + products.push_back(product_parse.second); + } + + std::vector reactants{}; + for (const auto& reactant : object[validation::keys.reactants]) + { + auto reactant_parse = ParseReactionComponent(reactant); + status = reactant_parse.first; + if (status != ConfigParseStatus::Success) + { + break; + } + reactants.push_back(reactant_parse.second); + } + + if (object.contains(validation::keys.A)) + { + aqueous_equilibrium.A = object[validation::keys.A].get(); + } + if (object.contains(validation::keys.C)) + { + aqueous_equilibrium.C = object[validation::keys.C].get(); + } + + aqueous_equilibrium.k_reverse = object[validation::keys.k_reverse].get(); + + if (object.contains(validation::keys.name)) + { + aqueous_equilibrium.name = object[validation::keys.name].get(); + } + + auto comments = GetComments(object, validation::aqueous_equilibrium.required_keys, validation::aqueous_equilibrium.optional_keys); + + std::unordered_map unknown_properties; + for (const auto& key : comments) + { + std::string val = object[key].dump(); + unknown_properties[key] = val; + } + + std::string aerosol_phase = object[validation::keys.aerosol_phase].get(); + std::string aerosol_phase_water = object[validation::keys.aerosol_phase_water].get(); + + std::vector requested_species; + for (const auto& spec : products) + { + requested_species.push_back(spec.species_name); + } + for (const auto& spec : reactants) + { + requested_species.push_back(spec.species_name); + } + requested_species.push_back(aerosol_phase_water); + + if (status == ConfigParseStatus::Success && RequiresUnknownSpecies(requested_species, existing_species)) + { + status = ConfigParseStatus::ReactionRequiresUnknownSpecies; + } + + auto phase_it = std::find_if( + existing_phases.begin(), existing_phases.end(), [&aerosol_phase](const types::Phase& phase) { return phase.name == aerosol_phase; }); + + if (phase_it != existing_phases.end()) + { + // check if all of the species for this reaction are actually in the aerosol phase + std::vector aerosol_phase_species = { (*phase_it).species.begin(), (*phase_it).species.end() }; + if (status == ConfigParseStatus::Success && RequiresUnknownSpecies(requested_species, aerosol_phase_species)) + { + status = ConfigParseStatus::RequestedAerosolSpeciesNotIncludedInAerosolPhase; + } + } + else + { + status = ConfigParseStatus::UnknownPhase; + } + + aqueous_equilibrium.aerosol_phase = aerosol_phase; + aqueous_equilibrium.aerosol_phase_water = aerosol_phase_water; + aqueous_equilibrium.products = products; + aqueous_equilibrium.reactants = reactants; + aqueous_equilibrium.unknown_properties = unknown_properties; + } + + return { status, aqueous_equilibrium }; + } + /// @brief Parses a wet deposition reaction /// @param object A json object that should have information containing arrhenius parameters /// @param existing_species A list of species configured in a mechanism @@ -1577,6 +1689,16 @@ namespace open_atmos } reactions.first_order_loss.push_back(first_order_loss_parse.second); } + else if (type == validation::keys.AqueousPhaseEquilibrium_key) + { + auto aqueous_equilibrium_parse = ParseAqueousEquilibrium(object, existing_species, existing_phases); + status = aqueous_equilibrium_parse.first; + if (status != ConfigParseStatus::Success) + { + break; + } + reactions.aqueous_equilibrium.push_back(aqueous_equilibrium_parse.second); + } else if (type == validation::keys.WetDeposition_key) { auto wet_deposition_parse = ParseWetDeposition(object, existing_species, existing_phases); @@ -1638,7 +1760,7 @@ namespace open_atmos ConfigParseStatus status; types::Mechanism mechanism; - status = ValidateSchema(object, validation::configuration.required_keys, validation::configuration.optional_keys); + status = ValidateSchema(object, validation::mechanism.required_keys, validation::mechanism.optional_keys); if (status != ConfigParseStatus::Success) { diff --git a/test/integration/test_json_parser.cpp b/test/integration/test_json_parser.cpp index 41eb692..d0148b5 100644 --- a/test/integration/test_json_parser.cpp +++ b/test/integration/test_json_parser.cpp @@ -21,6 +21,7 @@ TEST(JsonParser, ParsesFullConfiguration) EXPECT_EQ(mechanism.reactions.condensed_phase_photolysis.size(), 1); EXPECT_EQ(mechanism.reactions.emission.size(), 1); EXPECT_EQ(mechanism.reactions.first_order_loss.size(), 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium.size(), 1); EXPECT_EQ(mechanism.reactions.henrys_law.size(), 1); EXPECT_EQ(mechanism.reactions.photolysis.size(), 1); EXPECT_EQ(mechanism.reactions.surface.size(), 1); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index f0177e6..fa30a42 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -19,6 +19,7 @@ create_standard_test(NAME parse_species SOURCES test_parse_species.cpp) create_standard_test(NAME parse_surface SOURCES test_parse_surface.cpp) create_standard_test(NAME parse_troe SOURCES test_parse_troe.cpp) create_standard_test(NAME parse_tunneling SOURCES test_parse_tunneling.cpp) +create_standard_test(NAME parse_aqueous_equilibrium SOURCES test_parse_aqueous_equilibrium.cpp) create_standard_test(NAME parse_wet_deposition SOURCES test_parse_wet_deposition.cpp) ################################################################################ diff --git a/test/unit/test_parse_aqueous_equilibrium.cpp b/test/unit/test_parse_aqueous_equilibrium.cpp new file mode 100644 index 0000000..132e0d0 --- /dev/null +++ b/test/unit/test_parse_aqueous_equilibrium.cpp @@ -0,0 +1,66 @@ +#include + +#include + +using namespace open_atmos::mechanism_configuration; + +TEST(JsonParser, CanParseValidAqueousEquilibriumReaction) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/aqueous_equilibrium/valid.json")); + EXPECT_EQ(status, ConfigParseStatus::Success); + + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium.size(), 2); + + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].name, "my aqueous eq"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].aerosol_phase, "aerosol"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].aerosol_phase_water, "H2O_aq"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].A, 1.14e-2); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].C, 2300.0); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].k_reverse, 0.32); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].reactants.size(), 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].reactants[0].species_name, "A"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].reactants[0].coefficient, 2); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].products.size(), 2); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].products[0].species_name, "B"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].products[0].coefficient, 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].products[1].species_name, "C"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].products[1].coefficient, 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[0].unknown_properties["__comment"], "\"GIF is pronounced with a hard g\""); + + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].aerosol_phase, "aerosol"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].aerosol_phase_water, "H2O_aq"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].A, 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].C, 0); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].k_reverse, 0.32); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].reactants.size(), 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].reactants[0].species_name, "A"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].reactants[0].coefficient, 2); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].products.size(), 2); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].products[0].species_name, "B"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].products[0].coefficient, 1); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].products[1].species_name, "C"); + EXPECT_EQ(mechanism.reactions.aqueous_equilibrium[1].products[1].coefficient, 1); +} + +TEST(JsonParser, AqueousEquilibriumDetectsUnknownSpecies) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/aqueous_equilibrium/unknown_species.json")); + EXPECT_EQ(status, ConfigParseStatus::ReactionRequiresUnknownSpecies); +} + +TEST(JsonParser, AqueousEquilibriumDetectsBadReactionComponent) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/aqueous_equilibrium/bad_reaction_component.json")); + EXPECT_EQ(status, ConfigParseStatus::RequiredKeyNotFound); +} + +TEST(JsonParser, AqueousEquilibriumDetectsUnknownPhase) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/aqueous_equilibrium/missing_phase.json")); + EXPECT_EQ(status, ConfigParseStatus::UnknownPhase); +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/aqueous_equilibrium/bad_reaction_component.json b/test/unit/unit_configs/reactions/aqueous_equilibrium/bad_reaction_component.json new file mode 100644 index 0000000..dddaf02 --- /dev/null +++ b/test/unit/unit_configs/reactions/aqueous_equilibrium/bad_reaction_component.json @@ -0,0 +1,53 @@ +{ + "version": "1.0.0", + "name": "Bad reaction component", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + }, + { + "name": "H2O_aq" + } + ], + "phases": [ + { + "name": "aerosol", + "species": [ + "A", + "B", + "C", + "H2O_aq" + ] + } + ], + "reactions": [ + { + "type": "AQUEOUS_EQUILIBRIUM", + "aerosol phase": "aerosol", + "aerosol-phase water": "H2O_aq", + "k_reverse": 0.32, + "reactants": [ + { + "Species name": "A", + "coefficient": 2 + } + ], + "products": [ + { + "Species name": "B", + "coefficient": 1 + }, + { + "Species name": "C", + "coefficient": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/aqueous_equilibrium/missing_phase.json b/test/unit/unit_configs/reactions/aqueous_equilibrium/missing_phase.json new file mode 100644 index 0000000..c7e6d52 --- /dev/null +++ b/test/unit/unit_configs/reactions/aqueous_equilibrium/missing_phase.json @@ -0,0 +1,43 @@ +{ + "version": "1.0.0", + "name": "Missing phase", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + }, + { + "name": "H2O_aq" + } + ], + "phases": [ ], + "reactions": [ + { + "type": "AQUEOUS_EQUILIBRIUM", + "aerosol phase": "aerosol", + "aerosol-phase water": "H2O_aq", + "k_reverse": 0.32, + "reactants": [ + { + "species name": "A", + "coefficient": 2 + } + ], + "products": [ + { + "species name": "B", + "coefficient": 1 + }, + { + "species name": "C", + "coefficient": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/aqueous_equilibrium/unknown_species.json b/test/unit/unit_configs/reactions/aqueous_equilibrium/unknown_species.json new file mode 100644 index 0000000..6a766df --- /dev/null +++ b/test/unit/unit_configs/reactions/aqueous_equilibrium/unknown_species.json @@ -0,0 +1,45 @@ +{ + "version": "1.0.0", + "name": "Unknown species", + "species": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "phases": [ + { + "name": "aerosol", + "species": [ + "A", + "B" + ] + } + ], + "reactions": [ + { + "type": "AQUEOUS_EQUILIBRIUM", + "aerosol phase": "aerosol", + "aerosol-phase water": "H2O_aq", + "k_reverse": 0.32, + "reactants": [ + { + "species name": "A", + "coefficient": 2 + } + ], + "products": [ + { + "species name": "B", + "coefficient": 1 + }, + { + "species name": "C", + "coefficient": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/aqueous_equilibrium/valid.json b/test/unit/unit_configs/reactions/aqueous_equilibrium/valid.json new file mode 100644 index 0000000..8011b7c --- /dev/null +++ b/test/unit/unit_configs/reactions/aqueous_equilibrium/valid.json @@ -0,0 +1,79 @@ +{ + "version": "1.0.0", + "name": "Valid aqueous equilibrium", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + }, + { + "name": "H2O_aq" + } + ], + "phases": [ + { + "name": "aerosol", + "species": [ + "A", + "B", + "C", + "H2O_aq" + ] + } + ], + "reactions": [ + { + "type": "AQUEOUS_EQUILIBRIUM", + "aerosol phase": "aerosol", + "aerosol-phase water": "H2O_aq", + "A": 1.14e-2, + "C": 2300.0, + "k_reverse": 0.32, + "reactants": [ + { + "species name": "A", + "coefficient": 2 + } + ], + "products": [ + { + "species name": "B", + "coefficient": 1 + }, + { + "species name": "C", + "coefficient": 1 + } + ], + "name": "my aqueous eq", + "__comment": "GIF is pronounced with a hard g" + }, + { + "type": "AQUEOUS_EQUILIBRIUM", + "aerosol phase": "aerosol", + "aerosol-phase water": "H2O_aq", + "k_reverse": 0.32, + "reactants": [ + { + "species name": "A", + "coefficient": 2 + } + ], + "products": [ + { + "species name": "B", + "coefficient": 1 + }, + { + "species name": "C", + "coefficient": 1 + } + ] + } + ] +} \ No newline at end of file