diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..edb2924 --- /dev/null +++ b/.clang-format @@ -0,0 +1,26 @@ +--- +BasedOnStyle: Google +AlignAfterOpenBracket: 'AlwaysBreak' +AllowAllConstructorInitializersOnNextLine: 'false' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AlignConsecutiveMacros: 'true' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: 'None' +AllowShortIfStatementsOnASingleLine: 'Never' +AllowShortLoopsOnASingleLine: 'false' +BreakBeforeBraces: Allman +BinPackArguments: 'false' +BinPackParameters: 'false' +Cpp11BracedListStyle: 'false' +ColumnLimit: 150 +IndentWidth: 2 +IndentPPDirectives: AfterHash +NamespaceIndentation: All +PackConstructorInitializers: 'Never' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeCtorInitializerColon: 'true' +SpaceBeforeInheritanceColon: 'true' +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: 'true' +SpaceInEmptyBlock: true +Standard: 'Latest' \ No newline at end of file diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..d5e3131 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,58 @@ +name: Clang-Format + +on: + push: + branches: + - main + +jobs: + format: + name: Run Clang-Format + runs-on: ubuntu-latest + + steps: + - name: Install Clang-Format + run: sudo apt-get update && sudo apt-get install clang-format + + - name: Check out code, run clang format, push changes + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Format code + run: | + find include -type f \( -name '*.hpp' -o -name '*.h' \) -exec clang-format -i --style=file {} + + find src -type f \( -name '*.cu' -o -name '*.hpp' -o -name '*.h' -o -name '*.cpp' \) -exec clang-format -i --style=file {} + + find test -type f \( -name '*.hpp' -o -name '*.h' -o -name '*.cpp' \) ! -path 'test/tutorial/*' -exec clang-format -i --style=file {} + + + - name: Check for changes + id: check-changes + run: | + git diff --exit-code + continue-on-error: true + + - name: Commit and push changes + # a failue of this step means changes were detected + if: steps.check-changes.outcome != 'success' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -am "Auto-format code using Clang-Format" || echo "No changes to commit" + + - name: Push changes to main-formatting branch + # a failue of this step means changes were detected + if: steps.check-changes.outcome != 'success' + run: | + git push origin HEAD:main-formatting + + - name: Create Pull Request + # a failue of this step means changes were detected + if: steps.check-changes.outcome != 'success' + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Auto-format code using Clang-Format" + title: "Auto-format code changes" + body: "This is an automated pull request to apply code formatting using Clang-Format." + branch: "main-formatting" \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bf196d..515931b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,11 @@ add_subdirectory(src) if(PROJECT_IS_TOP_LEVEL AND ENABLE_TESTS) enable_testing() add_subdirectory(test) + + # Copy example folder to use as test data + + add_custom_target(copy_example_configs ALL ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/examples ${CMAKE_BINARY_DIR}/examples) endif() ################################################################################ diff --git a/examples/full_configuration.json b/examples/full_configuration.json index cc0dfae..492e8b4 100644 --- a/examples/full_configuration.json +++ b/examples/full_configuration.json @@ -1,94 +1,83 @@ { - "camp-data": [ - { - "type": "RELATIVE_TOLERANCE", - "value": 1.0e-30 - }, + "version": "1.0.0", + "name": "Full Configuration", + "species": [ { "name": "A", - "type": "CHEM_SPEC", - "absolute tolerance": 1.0e-30 + "__absolute tolerance": 1.0e-30 }, { - "name": "B", - "type": "CHEM_SPEC", - "absolute tolerance": 1.0e-30 + "name": "B" }, { - "name": "C", - "type": "CHEM_SPEC", - "absolute tolerance": 1.0e-30 + "name": "C" }, { "name": "H2O2", - "type": "CHEM_SPEC", - "HLC(298K) [M Pa-1]": 1.011596348, - "HLC exp factor [K]": 6340, - "diffusion coeff [m2 s-1]": 1.46E-05, + "HLC(298K) [mol m-3 Pa-1]": 1.011596348, + "HLC exponential factor [K]": 6340, + "diffusion coefficient [m2 s-1]": 1.46E-05, "N star": 1.74, "molecular weight [kg mol-1]": 0.0340147, "density [kg m-3]": 1000.0, - "absolute tolerance": 1.0e-10 + "__absolute tolerance": 1.0e-10 }, { "name": "ethanol", - "type": "CHEM_SPEC", - "diffusion coeff [m2 s-1]": 0.95E-05, + "diffusion coefficient [m2 s-1]": 0.95E-05, "N star": 2.55, "molecular weight [kg mol-1]": 0.04607, - "absolute tolerance": 1.0e-20 + "__absolute tolerance": 1.0e-20 }, { "name": "ethanol_aq", - "type": "CHEM_SPEC", - "phase": "AEROSOL", "molecular weight [kg mol-1]": 0.04607, "density [kg m-3]": 1000.0, - "absolute tolerance": 1.0e-20 + "__absolute tolerance": 1.0e-20 }, { "name": "H2O2_aq", - "type": "CHEM_SPEC", - "phase": "AEROSOL", "molecular weight [kg mol-1]": 0.0340147, "density [kg m-3]": 1000.0, - "absolute tolerance": 1.0e-10 + "__absolute tolerance": 1.0e-10 }, { "name": "H2O_aq", - "type": "CHEM_SPEC", - "phase": "AEROSOL", "density [kg m-3]": 1000.0, "molecular weight [kg mol-1]": 0.01801 }, - { - "name": "aqueous aerosol", - "type": "AERO_PHASE", - "species": [ - "H2O2_aq", - "H2O_aq", - "ethanol_aq" - ] - }, { "name": "aerosol stuff", - "type": "CHEM_SPEC", - "phase": "AEROSOL", "molecular weight [kg mol-1]": 0.5, "density [kg m-3]": 1000.0, - "absolute tolerance": 1.0e-20 + "__absolute tolerance": 1.0e-20 }, { "name": "more aerosol stuff", - "type": "CHEM_SPEC", - "phase": "AEROSOL", "molecular weight [kg mol-1]": 0.2, "density [kg m-3]": 1000.0, - "absolute tolerance": 1.0e-20 + "__absolute tolerance": 1.0e-20 + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B", + "C" + ] + }, + { + "name": "aqueous aerosol", + "species": [ + "H2O2_aq", + "H2O_aq", + "ethanol_aq" + ] }, { "name": "surface reacting phase", - "type": "AERO_PHASE", "species": [ "aerosol stuff", "more aerosol stuff" @@ -96,286 +85,266 @@ }, { "name": "cloud", - "type": "AERO_PHASE", "species": [ "B", "C" ] + } + ], + "reactions": [ + { + "type": "HL_PHASE_TRANSFER", + "gas phase": "gas", + "gas-phase species": "H2O2", + "aerosol phase": "aqueous aerosol", + "aerosol-phase species": "H2O2_aq", + "aerosol-phase water": "H2O_aq", + "name": "my henry's law" + }, + { + "type": "SIMPOL_PHASE_TRANSFER", + "gas phase": "gas", + "gas-phase species": "ethanol", + "aerosol phase": "aqueous aerosol", + "aerosol-phase species": "ethanol_aq", + "B": [ + -1.97E+03, + 2.91E+00, + 1.96E-03, + -4.96E-01 + ], + "name": "my simpl" }, { - "name": "CMAQ_H2O2", - "type": "MECHANISM", - "reactions": [ + "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": [ { - "type": "CMAQ_H2O2", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "k1_A": 1476.0, - "k1_B": 60, - "k1_C": -398, - "k2_A": 4e-20, - "k2_B": 1.5, - "k2_C": -2, - "time unit": "MIN" - }, + "species name": "A", + "coefficient": 2 + } + ], + "products": [ { - "type": "CMAQ_OH_HNO3", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "k0_A": 1476.0, - "k0_B": 60, - "k0_C": -398, - "k2_A": 1350, - "k2_B": 58, - "k2_C": -450, - "k3_A": 4e-20, - "k3_B": 1.5, - "k3_C": -2, - "time unit": "MIN" + "species name": "B", + "coefficient": 1 }, { - "type": "HL_PHASE_TRANSFER", - "gas-phase species": "H2O2", - "aerosol phase": "aqueous aerosol", - "aerosol-phase species": "H2O2_aq", - "aerosol-phase water": "H2O_aq" - }, + "species name": "C", + "coefficient": 1 + } + ], + "name": "my aqueous eq" + }, + { + "type": "CONDENSED_PHASE_ARRHENIUS", + "aerosol phase": "aqueous aerosol", + "aerosol-phase water": "H2O_aq", + "reactants": [ { - "type": "SIMPOL_PHASE_TRANSFER", - "gas-phase species": "ethanol", - "aerosol phase": "aqueous aerosol", - "aerosol-phase species": "ethanol_aq", - "B": [ - -1.97E+03, - 2.91E+00, - 1.96E-03, - -4.96E-01 - ] + "species name": "H2O2_aq", + "coefficient": 1 }, { - "type": "AQUEOUS_EQUILIBRIUM", - "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": [ - { - "speices name": "A", - "coeff": 2 - } - ], - "products": [ - { - "speices name": "B", - "coeff": 1 - }, - { - "speices name": "C", - "coeff": 1 - } - ] - }, + "species name": "H2O_aq", + "coefficient": 1 + } + ], + "products": [ { - "type": "CONDENSED_PHASE_ARRHENIUS", - "aerosol phase": "aqueous aerosol", - "aerosol-phase water": "H2O_aq", - "units": "M", - "reactants": [ - { - "speices name": "A", - "coeff": 1 - }, - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ] - }, + "species name": "ethanol_aq", + "coefficient": 1 + } + ], + "name": "my condensed arrhenius" + }, + { + "type": "CONDENSED_PHASE_PHOTOLYSIS", + "aerosol phase": "aqueous aerosol", + "aerosol-phase water": "H2O_aq", + "reactants": [ { - "type": "CONDENSED_PHASE_PHOTOLYSIS", - "aerosol phase": "aqueous aerosol", - "aerosol-phase water": "H2O_aq", - "units": "M", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "scaling factor": 12.3 + "species name": "H2O2_aq", + "coefficient": 1 }, { - "type": "EMISSION", - "species": "B", - "scaling factor": 12.3 - }, + "species name": "H2O_aq", + "coefficient": 1 + } + ], + "products": [ { - "type": "FIRST_ORDER_LOSS", - "species": "B", - "scaling factor": 12.3 - }, + "species name": "ethanol_aq", + "coefficient": 1 + } + ], + "scaling factor": 12.3, + "name": "condensed photo B" + }, + { + "type": "EMISSION", + "gas phase": "gas", + "reactants": [ { - "type": "PHOTOLYSIS", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "scaling factor": 12.3 - }, + "species name": "B", + "coefficient": 1 + } + ], + "name": "my emission", + "scaling factor": 12.3 + }, + { + "type": "FIRST_ORDER_LOSS", + "gas phase": "gas", + "products": [ { - "type": "SURFACE", - "gas-phase reactant": "foo", - "reaction probability": 2.0e-2, - "gas-phase products": { - "bar": {}, - "baz": { - "yield": 0.4 - } - }, - "aerosol phase": "surface reacting phase" - }, + "species name": "C", + "coefficient": 1 + } + ], + "name": "my first order loss", + "scaling factor": 12.3 + }, + { + "type": "PHOTOLYSIS", + "gas phase": "gas", + "reactants": [ { - "type": "TERNARY_CHEMICAL_ACTIVATION", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "k0_A": 1.2e-6, - "k0_B": 167, - "k0_C": 3, - "kinf_A": 136e6, - "kinf_B": 5, - "kinf_C": 24, - "Fc": 0.9, - "N": 0.8, - "time unit": "MIN" - }, + "species name": "B", + "coefficient": 1 + } + ], + "products": [ { - "type": "TROE", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "k0_A": 1.2e-12, - "k0_B": 167, - "k0_C": 3, - "kinf_A": 136, - "kinf_B": 5, - "kinf_C": 24, - "Fc": 0.9, - "N": 0.8, - "time unit": "MIN" - }, + "species name": "C", + "coefficient": 1 + } + ], + "name": "photo B", + "scaling factor": 12.3 + }, + { + "type": "SURFACE", + "gas phase": "gas", + "gas-phase reactant": "foo", + "reaction probability": 2.0e-2, + "gas-phase products": { + "bar": {}, + "baz": { + "yield": 0.4 + } + }, + "aerosol phase": "surface reacting phase", + "name": "my surface" + }, + { + "type": "TROE", + "gas phase": "gas", + "reactants": [ { - "type": "WENNBERG_NO_RO2", - "reactants": [ - { - "speices name": "A", - "coeff": 1 - } - ], - "alkoxy products": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "nitrate products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "X": 1.2e-4, - "Y": 167, - "a0": 0.15, - "n": 9, - "time unit": "MIN" + "species name": "B", + "coefficient": 1 }, { - "type": "WET_DEPOSITION", - "aerosol phase": "cloud", - "scaling factor": 12.3 - }, + "species name": "M", + "coefficient": 1 + } + ], + "products": [ { - "type": "ARRHENIUS", - "reactants": [ - { - "speices name": "B", - "coeff": 1 - } - ], - "products": [ - { - "speices name": "C", - "coeff": 1 - } - ], - "A": 32.1, - "B": -2.3, - "C": 102.3, - "D": 63.4, - "E": -1.3 + "species name": "C", + "coefficient": 1 + } + ], + "k0_A": 1.2e-12, + "k0_B": 167, + "k0_C": 3, + "kinf_A": 136, + "kinf_B": 5, + "kinf_C": 24, + "Fc": 0.9, + "N": 0.8, + "name": "my troe" + }, + { + "type": "BRANCHED_NO_RO2", + "gas phase": "gas", + "reactants": [ + { + "species name": "A", + "coefficient": 1 + } + ], + "alkoxy products": [ + { + "species name": "B", + "coefficient": 1 + } + ], + "nitrate products": [ + { + "species name": "C", + "coefficient": 1 + } + ], + "X": 1.2e-4, + "Y": 167, + "a0": 0.15, + "n": 9, + "name": "my branched" + }, + { + "type" : "TUNNELING", + "A" : 123.45, + "B" : 1200.0, + "C" : 1.0e8, + "reactants": [ + { + "species name": "B", + "coefficient": 1 + } + ], + "products": [ + { + "species name": "C", + "coefficient": 1 } ] + }, + { + "type": "WET_DEPOSITION", + "gas phase": "gas", + "aerosol phase": "cloud", + "name": "rxn cloud", + "scaling factor": 12.3 + }, + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "B", + "coefficient": 1 + } + ], + "products": [ + { + "species name": "C", + "coefficient": 1 + } + ], + "A": 32.1, + "B": -2.3, + "C": 102.3, + "D": 63.4, + "E": -1.3, + "name": "my arrhenius" } ] } \ No newline at end of file diff --git a/include/open_atmos/constants.hpp b/include/open_atmos/constants.hpp new file mode 100644 index 0000000..234cd5c --- /dev/null +++ b/include/open_atmos/constants.hpp @@ -0,0 +1,14 @@ +// Copyright (C) 2023-2024 National Center for Atmospheric Research, University of Illinois at Urbana-Champaign +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +namespace open_atmos +{ + namespace constants + { + static constexpr double boltzmann = 1.380649e-23; // J K^{-1} + static constexpr double avogadro = 6.02214076e23; // # mol^{-1} + static constexpr double R = boltzmann * avogadro; // J K^{-1} mol^{-1} + } // namespace constants +} // namespace open_atmos \ No newline at end of file diff --git a/include/open_atmos/mechanism_configuration/conversions.hpp b/include/open_atmos/mechanism_configuration/conversions.hpp new file mode 100644 index 0000000..5eea4ae --- /dev/null +++ b/include/open_atmos/mechanism_configuration/conversions.hpp @@ -0,0 +1,7 @@ +// Copyright (C) 2023-2024 National Center for Atmospheric Research, University of Illinois at Urbana-Champaign +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +constexpr double MolesM3ToMoleculesCm3 = 1.0e-6 * 6.02214076e23; \ No newline at end of file diff --git a/include/open_atmos/mechanism_configuration/parser.hpp b/include/open_atmos/mechanism_configuration/parser.hpp index d3f0645..6d306ed 100644 --- a/include/open_atmos/mechanism_configuration/parser.hpp +++ b/include/open_atmos/mechanism_configuration/parser.hpp @@ -4,10 +4,15 @@ #pragma once -#include #include #include #include +#include +#include +#include +#include +#include +#include namespace open_atmos { @@ -19,81 +24,35 @@ namespace open_atmos None, InvalidKey, UnknownKey, - InvalidCAMPFilePath, - NoConfigFilesFound, - CAMPFilesSectionNotFound, - CAMPDataSectionNotFound, - InvalidSpecies, - InvalidMechanism, + InvalidFilePath, ObjectTypeNotFound, RequiredKeyNotFound, - ContainsNonStandardKey, - MutuallyExclusiveOption + MutuallyExclusiveOption, + InvalidVersion, + DuplicateSpeciesDetected, + DuplicatePhasesDetected, + PhaseRequiresUnknownSpecies, + ReactionRequiresUnknownSpecies, }; + std::string configParseStatusToString(const ConfigParseStatus &status); - constexpr double MolesM3ToMoleculesCm3 = 1.0e-6 * 6.02214076e23; - - inline std::string configParseStatusToString(const ConfigParseStatus &status) - { - switch (status) - { - case ConfigParseStatus::Success: - return "Success"; - case ConfigParseStatus::None: - return "None"; - case ConfigParseStatus::InvalidKey: - return "InvalidKey"; - case ConfigParseStatus::UnknownKey: - return "UnknownKey"; - case ConfigParseStatus::InvalidCAMPFilePath: - return "InvalidCAMPFilePath"; - case ConfigParseStatus::NoConfigFilesFound: - return "NoConfigFilesFound"; - case ConfigParseStatus::CAMPFilesSectionNotFound: - return "CAMPFilesSectionNotFound"; - case ConfigParseStatus::CAMPDataSectionNotFound: - return "CAMPDataSectionNotFound"; - case ConfigParseStatus::InvalidSpecies: - return "InvalidSpecies"; - case ConfigParseStatus::InvalidMechanism: - return "InvalidMechanism"; - case ConfigParseStatus::ObjectTypeNotFound: - return "ObjectTypeNotFound"; - case ConfigParseStatus::RequiredKeyNotFound: - return "RequiredKeyNotFound"; - case ConfigParseStatus::ContainsNonStandardKey: - return "ContainsNonStandardKey"; - case ConfigParseStatus::MutuallyExclusiveOption: - return "MutuallyExclusiveOption"; - default: - return "Unknown"; - } - } - - class JsonReaderPolicy - { - public: - - /// @brief Parse configures - /// @param config_path Path to a the CAMP configuration directory or file - /// @return True for successful parsing - ConfigParseStatus Parse(const std::filesystem::path &config_path); - - private: - }; - - /// @brief Public interface to read and parse config - template - class ConfigurationReader : public ConfigTypePolicy + class JsonParser { - public: - /// @brief Reads and parses configures - /// @param config_dir Path to a the configuration directory - /// @return an enum indicating the success or failure of the parse - [[nodiscard]] ConfigParseStatus ReadAndParse(const std::filesystem::path &config_dir) - { - return this->Parse(config_dir); - } + public: + /// @brief Reads a configuration from a json object + /// @param object a json object + /// @return A pair containing the parsing status and mechanism + std::pair Parse(const nlohmann::json &object); + + /// @brief Reads a configuration from a file path + /// @param file_path A path to single json configuration + /// @return A pair containing the parsing status and mechanism + std::pair Parse(const std::filesystem::path &file_path); + + /// @brief Reads a configuration from a file path + /// @param file_path A path to single json configuration + /// @return A pair containing the parsing status and mechanism + std::pair Parse(const std::string &file_path); }; - } -} + } // namespace mechanism_configuration +} // namespace open_atmos diff --git a/include/open_atmos/mechanism_configuration/validation.hpp b/include/open_atmos/mechanism_configuration/validation.hpp new file mode 100644 index 0000000..6f5de43 --- /dev/null +++ b/include/open_atmos/mechanism_configuration/validation.hpp @@ -0,0 +1,99 @@ +// Copyright (C) 2023-2024 National Center for Atmospheric Research, University of Illinois at Urbana-Champaign +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace open_atmos +{ + namespace validation + { + struct Keys + { + // Shared, but also Mechanism + const std::string version = "version"; + const std::string name = "name"; + + // Configuration + const std::string species = "species"; + const std::string phases = "phases"; + const std::string reactions = "reactions"; + + // Species + const std::string absolute_tolerance = "absolute tolerance"; + const std::string diffusion_coefficient = "diffusion coefficient [m2 s-1]"; + const std::string molecular_weight = "molecular weight [kg mol-1]"; + const std::string henrys_law_constant_298 = "HLC(298K) [mol m-3 Pa-1]"; + const std::string henrys_law_constant_exponential_factor = "HLC exponential factor [K]"; + const std::string phase = "phase"; + const std::string n_star = "N star"; + const std::string density = "density [kg m-3]"; + + // Reactions + const std::string reactants = "reactants"; + const std::string products = "products"; + const std::string type = "type"; + const std::string gas_phase = "gas phase"; + + // Reactant and product + const std::string species_name = "species name"; + const std::string coefficient = "coefficient"; + + // Arrhenius + const std::string Arrhenius_key = "ARRHENIUS"; + const std::string A = "A"; + const std::string B = "B"; + const std::string C = "C"; + const std::string D = "D"; + const std::string E = "E"; + const std::string Ea = "Ea"; + + } keys; + + struct Configuration + { + const std::vector required_keys{ keys.version, keys.species, keys.phases, keys.reactions }; + const std::vector optional_keys{ keys.name }; + } configuration; + + struct Species + { + const std::vector required_keys{ keys.name }; + const std::vector optional_keys{ keys.absolute_tolerance, + keys.diffusion_coefficient, + keys.molecular_weight, + keys.henrys_law_constant_298, + keys.henrys_law_constant_exponential_factor, + keys.n_star, + keys.density }; + } species; + + struct Phase + { + const std::vector required_keys{ keys.name, keys.species }; + const std::vector optional_keys{}; + } phase; + + struct ReactionComponent + { + const std::vector required_keys{ keys.species_name }; + const std::vector optional_keys{ keys.coefficient }; + } reaction_component; + + struct Arrhenius + { + const std::vector required_keys{ keys.products, keys.reactants, keys.type, keys.gas_phase }; + const std::vector optional_keys{ keys.A, keys.B, keys.C, keys.D, keys.E, keys.Ea, keys.name }; + } arrhenius; + + struct Mechanism + { + const std::vector required_keys{}; + const std::vector optional_keys{}; + } mechanism; + + } // namespace validation +} // namespace open_atmos \ No newline at end of file diff --git a/include/open_atmos/mechanism_configuration/version.hpp b/include/open_atmos/mechanism_configuration/version.hpp index af59680..7ceb4fe 100644 --- a/include/open_atmos/mechanism_configuration/version.hpp +++ b/include/open_atmos/mechanism_configuration/version.hpp @@ -9,7 +9,7 @@ namespace open_atmos extern "C" { #endif - const char* getmechanism_configurationVersion() + const char* getVersionString() { return "1.0.0"; } diff --git a/include/open_atmos/types.hpp b/include/open_atmos/types.hpp new file mode 100644 index 0000000..2d3e422 --- /dev/null +++ b/include/open_atmos/types.hpp @@ -0,0 +1,78 @@ +// Copyright (C) 2023-2024 National Center for Atmospheric Research, University of Illinois at Urbana-Champaign +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace open_atmos +{ + namespace types + { + struct Species + { + std::string name; + + std::map optional_numerical_properties; + + std::unordered_map unknown_properties; + }; + + struct Phase + { + std::string name; + std::vector species; + std::unordered_map unknown_properties; + }; + + struct ReactionComponent + { + std::string species_name; + double coefficient; + std::unordered_map unknown_properties; + }; + + struct Arrhenius + { + /// @brief Pre-exponential factor [(mol m−3)^(−(𝑛−1)) s−1] + double A{ 1 }; + /// @brief Unitless exponential factor + double B{ 0 }; + /// @brief Activation threshold, expected to be the negative activation energy divided by the boltzman constant + /// [-E_a / k_b), K] + double C{ 0 }; + /// @brief A factor that determines temperature dependence [K] + double D{ 300 }; + /// @brief A factor that determines pressure dependence [Pa-1] + double E{ 0 }; + + /// @brief A list of reactants + std::vector reactants; + /// @brief A list of products + std::vector products; + /// @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; + + std::unordered_map unknown_properties; + }; + + struct Reactions + { + std::vector arrhenius; + }; + + struct Mechanism + { + /// @brief An identifier, optional + std::string name; + std::vector species; + std::vector phases; + Reactions reactions; + }; + + } // namespace types +} // namespace open_atmos \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 186904e..861f1b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,4 +18,4 @@ target_include_directories(mechanism_configuration $ ) -target_link_libraries(mechanism_configuration PRIVATE nlohmann_json::nlohmann_json) \ No newline at end of file +target_link_libraries(mechanism_configuration PUBLIC nlohmann_json::nlohmann_json) \ No newline at end of file diff --git a/src/parser.cpp b/src/parser.cpp index 652d998..6bc9552 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -2,18 +2,512 @@ // // SPDX-License-Identifier: Apache-2.0 +#include #include -#include +#include +#include namespace open_atmos { namespace mechanism_configuration { - // explicit template instanatiation - template class ConfigurationReader; + using nlohmann::json; - ConfigParseStatus JsonReaderPolicy::Parse(const std::filesystem::path &config_path) { + std::string configParseStatusToString(const ConfigParseStatus& status) + { + switch (status) + { + case ConfigParseStatus::Success: return "Success"; + case ConfigParseStatus::None: return "None"; + case ConfigParseStatus::InvalidKey: return "InvalidKey"; + case ConfigParseStatus::UnknownKey: return "UnknownKey"; + case ConfigParseStatus::InvalidFilePath: return "InvalidFilePath"; + case ConfigParseStatus::ObjectTypeNotFound: return "ObjectTypeNotFound"; + case ConfigParseStatus::RequiredKeyNotFound: return "RequiredKeyNotFound"; + case ConfigParseStatus::MutuallyExclusiveOption: return "MutuallyExclusiveOption"; + case ConfigParseStatus::DuplicateSpeciesDetected: return "DuplicateSpeciesDetected"; + case ConfigParseStatus::DuplicatePhasesDetected: return "DuplicatePhasesDetected"; + case ConfigParseStatus::PhaseRequiresUnknownSpecies: return "PhaseRequiresUnknownSpecies"; + case ConfigParseStatus::ReactionRequiresUnknownSpecies: return "ReactionRequiresUnknownSpecies"; + default: return "Unknown"; + } + } + + // Returns a vector for the allowed nonstandard keys, those that start with two underscores, like "__absolute tolerance" + std::vector + GetComments(const json& object, const std::vector& required_keys, const std::vector& optional_keys) + { + // standard keys are: + // those in required keys + // those in optional keys + // starting with __ + // anything else is reported as an error so that typos are caught, specifically for optional keys + + std::vector sorted_object_keys; + for (auto& [key, value] : object.items()) + sorted_object_keys.push_back(key); + + auto sorted_required_keys = required_keys; + auto sorted_optional_keys = optional_keys; + std::sort(sorted_object_keys.begin(), sorted_object_keys.end()); + std::sort(sorted_required_keys.begin(), sorted_required_keys.end()); + std::sort(sorted_optional_keys.begin(), sorted_optional_keys.end()); + + // get the difference between the object keys and those required + // what's left should be the optional keys and valid comments + std::vector difference; + std::set_difference( + sorted_object_keys.begin(), + sorted_object_keys.end(), + sorted_required_keys.begin(), + sorted_required_keys.end(), + std::back_inserter(difference)); + + std::vector remaining; + std::set_difference( + difference.begin(), difference.end(), sorted_optional_keys.begin(), sorted_optional_keys.end(), std::back_inserter(remaining)); + + return remaining; + } + + /// @brief Search for nonstandard keys. Only nonstandard keys starting with __ are allowed. Others are considered typos + /// @param object the object whose keys need to be validated + /// @param required_keys The required keys + /// @param optional_keys The optional keys + /// @return true if only standard keys are found + ConfigParseStatus ValidateSchema(const json& object, const std::vector& required_keys, const std::vector& optional_keys) + { + // standard keys are: + // those in required keys + // those in optional keys + // starting with __ + // anything else is reported as an error so that typos are caught, specifically for optional keys + + // debug statement + // std::cout << "ValidateSchema object " << object.dump(4) << std::endl; + + if (!object.empty() && object.begin().value().is_null()) + { + return ConfigParseStatus::Success; + } + + std::vector sorted_object_keys; + for (auto& [key, value] : object.items()) + sorted_object_keys.push_back(key); + + auto sorted_required_keys = required_keys; + auto sorted_optional_keys = optional_keys; + std::sort(sorted_object_keys.begin(), sorted_object_keys.end()); + std::sort(sorted_required_keys.begin(), sorted_required_keys.end()); + std::sort(sorted_optional_keys.begin(), sorted_optional_keys.end()); + + // get the difference between the object keys and those required + // what's left should be the optional keys and valid comments + std::vector difference; + std::set_difference( + sorted_object_keys.begin(), + sorted_object_keys.end(), + sorted_required_keys.begin(), + sorted_required_keys.end(), + std::back_inserter(difference)); + + // check that the number of keys remaining is exactly equal to the expected number of required keys + if (difference.size() != (sorted_object_keys.size() - required_keys.size())) + { + std::vector missing_keys; + std::set_difference( + sorted_required_keys.begin(), + sorted_required_keys.end(), + sorted_object_keys.begin(), + sorted_object_keys.end(), + std::back_inserter(missing_keys)); + for (auto& key : missing_keys) + std::cerr << "Missing required key '" << key << "' in object: " << object << std::endl; + + return ConfigParseStatus::RequiredKeyNotFound; + } + + std::vector remaining; + std::set_difference( + difference.begin(), difference.end(), sorted_optional_keys.begin(), sorted_optional_keys.end(), std::back_inserter(remaining)); + + // now, anything left must be standard comment starting with __ + for (auto& key : remaining) + { + if (!key.starts_with("__")) + { + std::cerr << "Non-standard key '" << key << "' found in object" << object << std::endl; + + return ConfigParseStatus::InvalidKey; + } + } return ConfigParseStatus::Success; } - } -} \ No newline at end of file + + template + bool ContainsUniqueObjectsByName(const std::vector& collection) + { + for (size_t i = 0; i < collection.size(); ++i) + { + for (size_t j = i + 1; j < collection.size(); ++j) + { + if (collection[i].name == collection[j].name) + { + return false; + } + } + } + return true; + } + + bool RequiresUnknownSpecies(const std::vector requested_species, const std::vector& existing_species) + { + for (const auto& spec : requested_species) + { + auto it = + std::find_if(existing_species.begin(), existing_species.end(), [&spec](const types::Species& existing) { return existing.name == spec; }); + + if (it == existing_species.end()) + { + return true; + } + } + return false; + } + + std::pair> ParseSpecies(const json& objects) + { + ConfigParseStatus status = ConfigParseStatus::Success; + std::vector all_species; + + for (const auto& object : objects) + { + types::Species species; + status = ValidateSchema(object, validation::species.required_keys, validation::species.optional_keys); + if (status != ConfigParseStatus::Success) + { + break; + } + + std::string name = object[validation::keys.name].get(); + + std::map numerical_properties{}; + for (const auto& key : validation::species.optional_keys) + { + if (object.contains(key)) + { + double val = object[key].get(); + numerical_properties[key] = val; + } + } + + auto comments = GetComments(object, validation::species.required_keys, validation::species.optional_keys); + + std::unordered_map unknown_properties; + for (const auto& key : comments) + { + std::string val = object[key].dump(); + unknown_properties[key] = val; + } + + species.name = name; + species.optional_numerical_properties = numerical_properties; + species.unknown_properties = unknown_properties; + + all_species.push_back(species); + } + + if (!ContainsUniqueObjectsByName(all_species)) + status = ConfigParseStatus::DuplicateSpeciesDetected; + + return { status, all_species }; + } + + std::pair> ParsePhases(const json& objects, const std::vector existing_species) + { + ConfigParseStatus status = ConfigParseStatus::Success; + std::vector all_phases; + + for (const auto& object : objects) + { + types::Phase phase; + status = ValidateSchema(object, validation::phase.required_keys, validation::phase.optional_keys); + if (status != ConfigParseStatus::Success) + { + break; + } + + std::string name = object[validation::keys.name].get(); + + std::vector species{}; + for (const auto& spec : object[validation::keys.species]) + { + species.push_back(spec); + } + + auto comments = GetComments(object, validation::phase.required_keys, validation::phase.optional_keys); + + std::unordered_map unknown_properties; + for (const auto& key : comments) + { + std::string val = object[key].dump(); + unknown_properties[key] = val; + } + + phase.name = name; + phase.species = species; + phase.unknown_properties = unknown_properties; + + if (RequiresUnknownSpecies(species, existing_species)) + { + status = ConfigParseStatus::PhaseRequiresUnknownSpecies; + break; + } + + all_phases.push_back(phase); + } + + if (status == ConfigParseStatus::Success && !ContainsUniqueObjectsByName(all_phases)) + status = ConfigParseStatus::DuplicatePhasesDetected; + + return { status, all_phases }; + } + + std::pair ParseReactionComponent(const json& object) + { + ConfigParseStatus status = ConfigParseStatus::Success; + types::ReactionComponent component; + + status = ValidateSchema(object, validation::reaction_component.required_keys, validation::reaction_component.optional_keys); + if (status == ConfigParseStatus::Success) + { + std::string species_name = object[validation::keys.species_name].get(); + double coefficient = 1; + if (object.contains(validation::keys.coefficient)) + { + coefficient = object[validation::keys.coefficient].get(); + } + + auto comments = GetComments(object, validation::reaction_component.required_keys, validation::reaction_component.optional_keys); + + std::unordered_map unknown_properties; + for (const auto& key : comments) + { + std::string val = object[key].dump(); + unknown_properties[key] = val; + } + + component.species_name = species_name; + component.coefficient = coefficient; + component.unknown_properties = unknown_properties; + } + + return { status, component }; + } + + std::pair ParseArrhenius(const json& object, const std::vector existing_species) + { + ConfigParseStatus status = ConfigParseStatus::Success; + types::Arrhenius arrhenius; + + status = ValidateSchema(object, validation::arrhenius.required_keys, validation::arrhenius.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)) + { + arrhenius.A = object[validation::keys.A].get(); + } + if (object.contains(validation::keys.B)) + { + arrhenius.B = object[validation::keys.B].get(); + } + if (object.contains(validation::keys.C)) + { + arrhenius.C = object[validation::keys.C].get(); + } + if (object.contains(validation::keys.D)) + { + arrhenius.D = object[validation::keys.D].get(); + } + if (object.contains(validation::keys.E)) + { + arrhenius.E = object[validation::keys.E].get(); + } + if (object.contains(validation::keys.Ea)) + { + if (arrhenius.C != 0) + { + std::cerr << "Ea is specified when C is also specified for an Arrhenius reaction. Pick one." << std::endl; + status = ConfigParseStatus::MutuallyExclusiveOption; + } + // Calculate 'C' using 'Ea' + arrhenius.C = -1 * object[validation::keys.Ea].get() / constants::boltzmann; + } + + if (object.contains(validation::keys.name)) + { + arrhenius.name = object[validation::keys.name].get(); + } + + auto comments = GetComments(object, validation::arrhenius.required_keys, validation::arrhenius.optional_keys); + + std::unordered_map unknown_properties; + for (const auto& key : comments) + { + std::string val = object[key].dump(); + unknown_properties[key] = val; + } + + 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); + } + + if (status == ConfigParseStatus::Success && RequiresUnknownSpecies(requested_species, existing_species)) { + status = ConfigParseStatus::ReactionRequiresUnknownSpecies; + } + + arrhenius.gas_phase = object[validation::keys.gas_phase].get(); + arrhenius.products = products; + arrhenius.reactants = reactants; + arrhenius.unknown_properties = unknown_properties; + } + + return { status, arrhenius }; + } + + std::pair ParseReactions(const json& objects, const std::vector existing_species) + { + ConfigParseStatus status = ConfigParseStatus::Success; + types::Reactions reactions; + + for (const auto& object : objects) + { + std::string type = object[validation::keys.type].get(); + if (type == validation::keys.Arrhenius_key) + { + auto arrhenius_parse = ParseArrhenius(object, existing_species); + status = arrhenius_parse.first; + if (status != ConfigParseStatus::Success) + { + break; + } + reactions.arrhenius.push_back(arrhenius_parse.second); + } + } + + return { status, reactions }; + } + + std::pair JsonParser::Parse(const std::string& file_path) + { + return JsonParser::Parse(std::filesystem::path(file_path)); + } + + std::pair JsonParser::Parse(const std::filesystem::path& file_path) + { + ConfigParseStatus status; + + if (!std::filesystem::exists(file_path) || std::filesystem::is_directory(file_path)) + { + status = ConfigParseStatus::InvalidFilePath; + std::string msg = configParseStatusToString(status); + std::cerr << msg << std::endl; + return { status, types::Mechanism() }; + } + + json config = json::parse(std::ifstream(file_path)); + + return JsonParser::Parse(config); + } + + std::pair JsonParser::Parse(const nlohmann::json& object) + { + ConfigParseStatus status; + types::Mechanism mechanism; + + status = ValidateSchema(object, validation::configuration.required_keys, validation::configuration.optional_keys); + + if (status != ConfigParseStatus::Success) + { + std::string msg = configParseStatusToString(status); + std::cerr << "[" << msg << "] Invalid top level configuration." << std::endl; + return { status, mechanism }; + } + + std::string version = object[validation::keys.version].get(); + + if (version != getVersionString()) + { + status = ConfigParseStatus::InvalidVersion; + std::string msg = configParseStatusToString(status); + std::cerr << "[" << msg << "] This parser supports version " << getVersionString() << " and you requested version " << version + << ". Please download the appropriate version of the parser or switch to the supported format's version." << std::endl; + } + + std::string name = object[validation::keys.name].get(); + mechanism.name = name; + + // parse all of the species at once + auto species_parsing = ParseSpecies(object[validation::keys.species]); + + if (species_parsing.first != ConfigParseStatus::Success) + { + status = species_parsing.first; + std::string msg = configParseStatusToString(status); + std::cerr << "[" << msg << "] Failed to parse the species." << std::endl; + } + + // parse all of the phases at once + auto phases_parsing = ParsePhases(object[validation::keys.phases], species_parsing.second); + + if (phases_parsing.first != ConfigParseStatus::Success) + { + status = phases_parsing.first; + std::string msg = configParseStatusToString(status); + std::cerr << "[" << msg << "] Failed to parse the phases." << std::endl; + } + + // parse all of the reactions at once + auto reactions_parsing = ParseReactions(object[validation::keys.reactions], species_parsing.second); + + if (reactions_parsing.first != ConfigParseStatus::Success) + { + status = reactions_parsing.first; + std::string msg = configParseStatusToString(status); + std::cerr << "[" << msg << "] Failed to parse the reactions." << std::endl; + } + + mechanism.species = species_parsing.second; + mechanism.phases = phases_parsing.second; + mechanism.reactions = reactions_parsing.second; + + return { status, mechanism }; + } + } // namespace mechanism_configuration +} // namespace open_atmos diff --git a/src/version.hpp.in b/src/version.hpp.in index 2948896..3274919 100644 --- a/src/version.hpp.in +++ b/src/version.hpp.in @@ -9,7 +9,7 @@ namespace open_atmos extern "C" { #endif - const char* getmechanism_configurationVersion() + const char* getVersionString() { return "@mechanism_configuration_VERSION@"; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 059f2a2..e138dc8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1 +1,2 @@ +add_subdirectory(integration) add_subdirectory(unit) \ No newline at end of file diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt new file mode 100644 index 0000000..5380dda --- /dev/null +++ b/test/integration/CMakeLists.txt @@ -0,0 +1,9 @@ +################################################################################ +# Test utilities + +include(test_util) + +################################################################################ +# Tests + +create_standard_test(NAME json_parser SOURCES test_json_parser.cpp) \ No newline at end of file diff --git a/test/integration/test_json_parser.cpp b/test/integration/test_json_parser.cpp new file mode 100644 index 0000000..c49b2de --- /dev/null +++ b/test/integration/test_json_parser.cpp @@ -0,0 +1,22 @@ +#include + +#include + +using namespace open_atmos::mechanism_configuration; + +TEST(JsonParser, ParsesFullConfiguration) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("examples/full_configuration.json")); + EXPECT_EQ(status, ConfigParseStatus::Success); + EXPECT_EQ(mechanism.name, "Full Configuration"); + EXPECT_EQ(mechanism.species.size(), 10); + EXPECT_EQ(mechanism.reactions.arrhenius.size(), 1); +} + +TEST(JsonParser, ParserReportsBadFiles) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("examples/_missing_configuration.json")); + EXPECT_EQ(status, ConfigParseStatus::InvalidFilePath); +} \ No newline at end of file diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 3fc656b..b0410c8 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -6,7 +6,9 @@ include(test_util) ################################################################################ # Tests -create_standard_test(NAME parser SOURCES test_parser.cpp) +create_standard_test(NAME parse_species SOURCES test_parse_species.cpp) +create_standard_test(NAME parse_phases SOURCES test_parse_phases.cpp) +create_standard_test(NAME parse_arrhenius SOURCES test_parse_arrhenius.cpp) ################################################################################ # Copy test data diff --git a/test/unit/test_parse_arrhenius.cpp b/test/unit/test_parse_arrhenius.cpp new file mode 100644 index 0000000..e26fba1 --- /dev/null +++ b/test/unit/test_parse_arrhenius.cpp @@ -0,0 +1,85 @@ +#include + +#include + +using namespace open_atmos::mechanism_configuration; + +TEST(JsonParser, CanParseValidArrheniusReaction) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/arrhenius/valid.json")); + EXPECT_EQ(status, ConfigParseStatus::Success); + + EXPECT_EQ(mechanism.reactions.arrhenius.size(), 3); + + EXPECT_EQ(mechanism.reactions.arrhenius[0].name, "my arrhenius"); + EXPECT_EQ(mechanism.reactions.arrhenius[0].gas_phase, "gas"); + EXPECT_EQ(mechanism.reactions.arrhenius[0].A, 32.1); + EXPECT_EQ(mechanism.reactions.arrhenius[0].B, -2.3); + EXPECT_EQ(mechanism.reactions.arrhenius[0].C, 102.3); + EXPECT_EQ(mechanism.reactions.arrhenius[0].D, 63.4); + EXPECT_EQ(mechanism.reactions.arrhenius[0].E, -1.3); + EXPECT_EQ(mechanism.reactions.arrhenius[0].reactants.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[0].reactants[0].species_name, "A"); + EXPECT_EQ(mechanism.reactions.arrhenius[0].reactants[0].coefficient, 1); + EXPECT_EQ(mechanism.reactions.arrhenius[0].products.size(), 2); + EXPECT_EQ(mechanism.reactions.arrhenius[0].products[0].species_name, "B"); + EXPECT_EQ(mechanism.reactions.arrhenius[0].products[0].coefficient, 1.2); + EXPECT_EQ(mechanism.reactions.arrhenius[0].products[1].species_name, "C"); + EXPECT_EQ(mechanism.reactions.arrhenius[0].products[1].coefficient, 0.3); + EXPECT_EQ(mechanism.reactions.arrhenius[0].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[0].unknown_properties["__solver_param"], "0.1"); + + EXPECT_EQ(mechanism.reactions.arrhenius[1].name, "my arrhenius2"); + EXPECT_EQ(mechanism.reactions.arrhenius[1].gas_phase, "gas"); + EXPECT_EQ(mechanism.reactions.arrhenius[1].A, 3.1); + EXPECT_EQ(mechanism.reactions.arrhenius[1].B, -0.3); + EXPECT_EQ(mechanism.reactions.arrhenius[1].C, 12.3); + EXPECT_EQ(mechanism.reactions.arrhenius[1].D, 6.4); + EXPECT_EQ(mechanism.reactions.arrhenius[1].E, -0.3); + EXPECT_EQ(mechanism.reactions.arrhenius[1].reactants.size(), 2); + EXPECT_EQ(mechanism.reactions.arrhenius[1].reactants[0].species_name, "A"); + EXPECT_EQ(mechanism.reactions.arrhenius[1].reactants[0].coefficient, 2); + EXPECT_EQ(mechanism.reactions.arrhenius[1].reactants[1].species_name, "B"); + EXPECT_EQ(mechanism.reactions.arrhenius[1].reactants[1].coefficient, 0.1); + EXPECT_EQ(mechanism.reactions.arrhenius[1].products.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[1].products[0].species_name, "C"); + EXPECT_EQ(mechanism.reactions.arrhenius[1].products[0].coefficient, 0.5); + EXPECT_EQ(mechanism.reactions.arrhenius[1].products[0].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[1].products[0].unknown_properties["__optional thing"], "\"hello\""); + + EXPECT_EQ(mechanism.reactions.arrhenius[2].name, ""); + EXPECT_EQ(mechanism.reactions.arrhenius[2].gas_phase, "gas"); + EXPECT_EQ(mechanism.reactions.arrhenius[2].A, 1); + EXPECT_EQ(mechanism.reactions.arrhenius[2].B, 0); + EXPECT_EQ(mechanism.reactions.arrhenius[2].C, 0); + EXPECT_EQ(mechanism.reactions.arrhenius[2].D, 300); + EXPECT_EQ(mechanism.reactions.arrhenius[2].E, 0); + EXPECT_EQ(mechanism.reactions.arrhenius[2].reactants.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[2].reactants[0].species_name, "A"); + EXPECT_EQ(mechanism.reactions.arrhenius[2].reactants[0].coefficient, 1); + EXPECT_EQ(mechanism.reactions.arrhenius[2].products.size(), 1); + EXPECT_EQ(mechanism.reactions.arrhenius[2].products[0].species_name, "C"); + EXPECT_EQ(mechanism.reactions.arrhenius[2].products[0].coefficient, 1); +} + +TEST(JsonParser, ArrheniusDetectsUnknownSpecies) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/arrhenius/unknown_species.json")); + EXPECT_EQ(status, ConfigParseStatus::ReactionRequiresUnknownSpecies); +} + +TEST(JsonParser, ArrheniusDetectsMutuallyExclusiveOptions) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/arrhenius/mutually_exclusive.json")); + EXPECT_EQ(status, ConfigParseStatus::MutuallyExclusiveOption); +} + +TEST(JsonParser, ArrheniusDetectsBadReactionComponent) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/reactions/arrhenius/bad_reaction_component.json")); + EXPECT_EQ(status, ConfigParseStatus::RequiredKeyNotFound); +} \ No newline at end of file diff --git a/test/unit/test_parse_phases.cpp b/test/unit/test_parse_phases.cpp new file mode 100644 index 0000000..2065294 --- /dev/null +++ b/test/unit/test_parse_phases.cpp @@ -0,0 +1,61 @@ +#include + +#include + +using namespace open_atmos::mechanism_configuration; + +TEST(JsonParser, CanParseValidPhases) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/phases/valid_phases.json")); + + EXPECT_EQ(status, ConfigParseStatus::Success); + EXPECT_EQ(mechanism.species.size(), 3); + EXPECT_EQ(mechanism.phases.size(), 2); + + EXPECT_EQ(mechanism.phases[0].name, "gas"); + EXPECT_EQ(mechanism.phases[0].species.size(), 2); + EXPECT_EQ(mechanism.phases[0].species[0], "A"); + EXPECT_EQ(mechanism.phases[0].species[1], "B"); + EXPECT_EQ(mechanism.phases[0].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.phases[0].unknown_properties["__other"], "\"key\""); + + EXPECT_EQ(mechanism.phases[1].name, "aerosols"); + EXPECT_EQ(mechanism.phases[1].species.size(), 1); + EXPECT_EQ(mechanism.phases[1].species[0], "C"); + EXPECT_EQ(mechanism.phases[1].unknown_properties.size(), 2); + EXPECT_EQ(mechanism.phases[1].unknown_properties["__other1"], "\"key1\""); + EXPECT_EQ(mechanism.phases[1].unknown_properties["__other2"], "\"key2\""); +} + +TEST(JsonParser, DetectsDuplicatePhases) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/phases/duplicate_phases.json")); + + EXPECT_EQ(status, ConfigParseStatus::DuplicatePhasesDetected); +} + +TEST(JsonParser, DetectsMissingRequiredKeys) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/phases/missing_required_key.json")); + + EXPECT_EQ(status, ConfigParseStatus::RequiredKeyNotFound); +} + +TEST(JsonParser, DetectsInvalidKeys) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/phases/invalid_key.json")); + + EXPECT_EQ(status, ConfigParseStatus::InvalidKey); +} + +TEST(JsonParser, DetectsPhaseRequestingUnknownSpecies) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/phases/unknown_species.json")); + + EXPECT_EQ(status, ConfigParseStatus::PhaseRequiresUnknownSpecies); +} \ No newline at end of file diff --git a/test/unit/test_parse_species.cpp b/test/unit/test_parse_species.cpp new file mode 100644 index 0000000..1fd250c --- /dev/null +++ b/test/unit/test_parse_species.cpp @@ -0,0 +1,60 @@ +#include + +#include + +using namespace open_atmos::mechanism_configuration; + +TEST(JsonParser, CanParseValidSpecies) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/species/valid_species.json")); + + EXPECT_EQ(status, ConfigParseStatus::Success); + EXPECT_EQ(mechanism.species.size(), 3); + + EXPECT_EQ(mechanism.species[0].name, "A"); + EXPECT_EQ(mechanism.species[0].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.species[0].unknown_properties["__absolute tolerance"], "1e-30"); + + EXPECT_EQ(mechanism.species[1].name, "H2O2"); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties.size(), 6); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["HLC(298K) [mol m-3 Pa-1]"], 1.011596348); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["HLC exponential factor [K]"], 6340); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["diffusion coefficient [m2 s-1]"], 1.46e-05); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["N star"], 1.74); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["molecular weight [kg mol-1]"], 0.0340147); + EXPECT_EQ(mechanism.species[1].optional_numerical_properties["density [kg m-3]"], 1000.0); + EXPECT_EQ(mechanism.species[1].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.species[1].unknown_properties["__absolute tolerance"], "1e-10"); + + EXPECT_EQ(mechanism.species[2].name, "aerosol stuff"); + EXPECT_EQ(mechanism.species[2].optional_numerical_properties.size(), 2); + EXPECT_EQ(mechanism.species[2].optional_numerical_properties["molecular weight [kg mol-1]"], 0.5); + EXPECT_EQ(mechanism.species[2].optional_numerical_properties["density [kg m-3]"], 1000.0); + EXPECT_EQ(mechanism.species[2].unknown_properties.size(), 1); + EXPECT_EQ(mechanism.species[2].unknown_properties["__absolute tolerance"], "1e-20"); +} + +TEST(JsonParser, DetectsDuplicateSpecies) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/species/duplicate_species.json")); + + EXPECT_EQ(status, ConfigParseStatus::DuplicateSpeciesDetected); +} + +TEST(JsonParser, DetectsMissingRequiredKeys) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/species/missing_required_key.json")); + + EXPECT_EQ(status, ConfigParseStatus::RequiredKeyNotFound); +} + +TEST(JsonParser, DetectsInvalidKeys) +{ + JsonParser parser; + auto [status, mechanism] = parser.Parse(std::string("unit_configs/species/invalid_key.json")); + + EXPECT_EQ(status, ConfigParseStatus::InvalidKey); +} \ No newline at end of file diff --git a/test/unit/test_parser.cpp b/test/unit/test_parser.cpp deleted file mode 100644 index b5b9873..0000000 --- a/test/unit/test_parser.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include - -#include - -using namespace open_atmos::mechanism_configuration; - -TEST(Parser, Returns) -{ - ConfigurationReader reader; -} \ No newline at end of file diff --git a/test/unit/unit_configs/.gitkeep b/test/unit/unit_configs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/unit_configs/phases/duplicate_phases.json b/test/unit/unit_configs/phases/duplicate_phases.json new file mode 100644 index 0000000..66da828 --- /dev/null +++ b/test/unit/unit_configs/phases/duplicate_phases.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0", + "name": "Duplicate phases configuration", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ] + }, + { + "name": "gas", + "species": [ + "A", + "B" + ] + } + ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/phases/invalid_key.json b/test/unit/unit_configs/phases/invalid_key.json new file mode 100644 index 0000000..0b57b70 --- /dev/null +++ b/test/unit/unit_configs/phases/invalid_key.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "name": "Invalid key configuration", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ], + "other": "key" + } + ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/phases/missing_required_key.json b/test/unit/unit_configs/phases/missing_required_key.json new file mode 100644 index 0000000..5f2c793 --- /dev/null +++ b/test/unit/unit_configs/phases/missing_required_key.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "name": "Missing required phases key configuration", + "species": [ + ], + "phases": [ + { + "name": "gas" + } + ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/phases/unknown_species.json b/test/unit/unit_configs/phases/unknown_species.json new file mode 100644 index 0000000..4a80e50 --- /dev/null +++ b/test/unit/unit_configs/phases/unknown_species.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "name": "Unknown species configuration", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "D" + ], + "__other": "key" + } + ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/phases/valid_phases.json b/test/unit/unit_configs/phases/valid_phases.json new file mode 100644 index 0000000..d208cd5 --- /dev/null +++ b/test/unit/unit_configs/phases/valid_phases.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0", + "name": "Valid phases configuration", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ], + "__other": "key" + }, + { + "name": "aerosols", + "species": [ + "C" + ], + "__other1": "key1", + "__other2": "key2" + } + ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/arrhenius/bad_reaction_component.json b/test/unit/unit_configs/reactions/arrhenius/bad_reaction_component.json new file mode 100644 index 0000000..859209d --- /dev/null +++ b/test/unit/unit_configs/reactions/arrhenius/bad_reaction_component.json @@ -0,0 +1,37 @@ +{ + "version": "1.0.0", + "name": "Mutually Exclusive", + "species": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ] + } + ], + "reactions": [ + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "Species name": "A" + } + ], + "products": [ + { + "species name": "B" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/arrhenius/mutually_exclusive.json b/test/unit/unit_configs/reactions/arrhenius/mutually_exclusive.json new file mode 100644 index 0000000..61db970 --- /dev/null +++ b/test/unit/unit_configs/reactions/arrhenius/mutually_exclusive.json @@ -0,0 +1,39 @@ +{ + "version": "1.0.0", + "name": "Mutually Exclusive", + "species": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ] + } + ], + "reactions": [ + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "A" + } + ], + "products": [ + { + "species name": "B" + } + ], + "C": 10, + "Ea": 0.5 + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/arrhenius/unknown_species.json b/test/unit/unit_configs/reactions/arrhenius/unknown_species.json new file mode 100644 index 0000000..a92afa3 --- /dev/null +++ b/test/unit/unit_configs/reactions/arrhenius/unknown_species.json @@ -0,0 +1,37 @@ +{ + "version": "1.0.0", + "name": "Unknown species", + "species": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B" + ] + } + ], + "reactions": [ + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "A" + } + ], + "products": [ + { + "species name": "C" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/reactions/arrhenius/valid.json b/test/unit/unit_configs/reactions/arrhenius/valid.json new file mode 100644 index 0000000..2ef1013 --- /dev/null +++ b/test/unit/unit_configs/reactions/arrhenius/valid.json @@ -0,0 +1,95 @@ +{ + "version": "1.0.0", + "name": "Valid arrhenius", + "species": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + } + ], + "phases": [ + { + "name": "gas", + "species": [ + "A", + "B", + "C" + ] + } + ], + "reactions": [ + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "A", + "coefficient": 1 + } + ], + "products": [ + { + "species name": "B", + "coefficient": 1.2 + }, + { + "species name": "C", + "coefficient": 0.3 + } + ], + "A": 32.1, + "B": -2.3, + "C": 102.3, + "D": 63.4, + "E": -1.3, + "name": "my arrhenius", + "__solver_param": 0.1 + }, + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "A", + "coefficient": 2 + }, + { + "species name": "B", + "coefficient": 0.1 + } + ], + "products": [ + { + "species name": "C", + "coefficient": 0.5, + "__optional thing": "hello" + } + ], + "A": 3.1, + "B": -0.3, + "C": 12.3, + "D": 6.4, + "E": -0.3, + "name": "my arrhenius2" + }, + { + "type": "ARRHENIUS", + "gas phase": "gas", + "reactants": [ + { + "species name": "A" + } + ], + "products": [ + { + "species name": "C" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/species/duplicate_species.json b/test/unit/unit_configs/species/duplicate_species.json new file mode 100644 index 0000000..ad41dbc --- /dev/null +++ b/test/unit/unit_configs/species/duplicate_species.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "name": "Duplicate Species", + "species": [ + { + "name": "A" + }, + { + "name": "A" + } + ], + "phases": [ ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/species/invalid_key.json b/test/unit/unit_configs/species/invalid_key.json new file mode 100644 index 0000000..5ae7360 --- /dev/null +++ b/test/unit/unit_configs/species/invalid_key.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "name": "Invalid key", + "species": [ + { + "name": "A", + "_absolute tolerance": 1.0e-30 + } + ], + "phases": [ ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/species/missing_required_key.json b/test/unit/unit_configs/species/missing_required_key.json new file mode 100644 index 0000000..0ac2635 --- /dev/null +++ b/test/unit/unit_configs/species/missing_required_key.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.0", + "name": "Invalid key", + "species": [ + { + "Name": "A" + } + ], + "phases": [ ], + "reactions": [ ] +} \ No newline at end of file diff --git a/test/unit/unit_configs/species/valid_species.json b/test/unit/unit_configs/species/valid_species.json new file mode 100644 index 0000000..d45ac2d --- /dev/null +++ b/test/unit/unit_configs/species/valid_species.json @@ -0,0 +1,28 @@ +{ + "version": "1.0.0", + "name": "Valid species configuration", + "species": [ + { + "name": "A", + "__absolute tolerance": 1.0e-30 + }, + { + "name": "H2O2", + "HLC(298K) [mol m-3 Pa-1]": 1.011596348, + "HLC exponential factor [K]": 6340, + "diffusion coefficient [m2 s-1]": 1.46E-05, + "N star": 1.74, + "molecular weight [kg mol-1]": 0.0340147, + "density [kg m-3]": 1000.0, + "__absolute tolerance": 1.0e-10 + }, + { + "name": "aerosol stuff", + "molecular weight [kg mol-1]": 0.5, + "density [kg m-3]": 1000.0, + "__absolute tolerance": 1.0e-20 + } + ], + "phases": [ ], + "reactions": [ ] +} \ No newline at end of file