From 54d39e6c3ea95658e7eac5271ce44940cd1b6188 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Tue, 23 Jun 2026 13:24:21 -0600 Subject: [PATCH 01/23] clean up the config file for cloud chemistry --- .../cam_clound_chemistry.json | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 test/integration/integration_configs/cam_clound_chemistry.json diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json new file mode 100644 index 00000000..f6d7c881 --- /dev/null +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -0,0 +1,232 @@ +{ + "version": "1.0.0", + "name": "CAM Cloud Chemistry", + + "species": [ + { "name": "SO2", "molecular weight [kg mol-1]": 0.06407 }, + { "name": "H2O2", "molecular weight [kg mol-1]": 0.03401 }, + { "name": "O3", "molecular weight [kg mol-1]": 0.04800 }, + { "name": "Hp", "molecular weight [kg mol-1]": 0.00101 }, + { "name": "OHm", "molecular weight [kg mol-1]": 0.01701 }, + { "name": "HSO3m", "molecular weight [kg mol-1]": 0.08107 }, + { "name": "SO3mm", "molecular weight [kg mol-1]": 0.08007 }, + { "name": "SO4mm", "molecular weight [kg mol-1]": 0.09607 }, + { "name": "SO2OOHm", "molecular weight [kg mol-1]": 0.11307 }, + { "name": "H2O", "molecular weight [kg mol-1]": 0.01801 } + ], + + "phases": [ + { + "name": "gas", + "species": [ + { "name": "SO2", "diffusion coefficient [m2 s-1]": 1.28e-5 }, + { "name": "H2O2", "diffusion coefficient [m2 s-1]": 1.46e-5 }, + { "name": "O3", "diffusion coefficient [m2 s-1]": 1.48e-5 } + ] + }, + { + "name": "AQUEOUS", + "species": [ + "SO2", "H2O2", "O3", "Hp", "OHm", "HSO3m", "SO3mm", "SO4mm", "SO2OOHm", + { "name": "H2O", "density [kg m-3]": 997.0 } + ] + } + ], + + "reactions": [], + + "aerosol representations": [ + { + "type": "UNIFORM_SECTION", + "name": "CLOUD", + "phases": ["AQUEOUS"], + "minimum radius [m]": 1.0e-6, + "maximum radius [m]": 1.0e-5 + } + ], + + "aerosol processes": [ + { + "__comment": "Henry's Law Equilibrium: SO2(g) <-> SO2(aq)", + "type": "HENRY_LAW_EQUILIBRIUM", + "gas phase": "gas", + "gas-phase species": "SO2", + "condensed phase": "AQUEOUS", + "condensed-phase species": "SO2", + "solvent": "H2O", + "Henry's law constant": { + "HLC_ref [mol m-3 Pa-1]": 1.214e-2, + "C [K]": 3120.0 + } + }, + { + "__comment": "Henry's Law Equilibrium: H2O2(g) <-> H2O2(aq)", + "type": "HENRY_LAW_EQUILIBRIUM", + "gas phase": "gas", + "gas-phase species": "H2O2", + "condensed phase": "AQUEOUS", + "condensed-phase species": "H2O2", + "solvent": "H2O", + "Henry's law constant": { + "HLC_ref [mol m-3 Pa-1]": 7.306e2, + "C [K]": 6621.0 + } + }, + { + "__comment": "Henry's Law Equilibrium: O3(g) <-> O3(aq)", + "type": "HENRY_LAW_EQUILIBRIUM", + "gas phase": "gas", + "gas-phase species": "O3", + "condensed phase": "AQUEOUS", + "condensed-phase species": "O3", + "solvent": "H2O", + "Henry's law constant": { + "HLC_ref [mol m-3 Pa-1]": 1.135e-4, + "C [K]": 2560.0 + } + }, + { + "__comment": "R1a: HSO3- + H2O2_aq <-> SO2OOH- + H2O (reversible, Keq=1725)", + "__note": "k_fwd = c_H2O_M * 7.45e7/13 = 3.184e8; Keq=1725 (Lind & Kok 1986 / revised MIAM)", + "type": "DISSOLVED_REVERSIBLE_REACTION", + "condensed phase": "AQUEOUS", + "solvent": "H2O", + "reactants": [ + { "species": "HSO3m", "coefficient": 1 }, + { "species": "H2O2", "coefficient": 1 } + ], + "products": [ + { "species": "SO2OOHm", "coefficient": 1 }, + { "species": "H2O", "coefficient": 1 } + ], + "forward rate constants": { + "CLOUD": { "A": 3.184e8, "C": 4430.0 } + }, + "equilibrium constant": { + "A": 1725.0 + } + }, + { + "__comment": "R1b: SO2OOH- + H+ -> SO4-- (irreversible)", + "__note": "k = c_H2O_M * 2.4e6 = 1.333e8; Ea/R = 4430 K", + "type": "DISSOLVED_REACTION", + "condensed phase": "AQUEOUS", + "solvent": "H2O", + "reactants": [ + { "species name": "SO2OOHm", "coefficient": 1 }, + { "species name": "Hp", "coefficient": 1 } + ], + "products": [ + { "species name": "SO4mm", "coefficient": 1 } + ], + "rate constants": { + "CLOUD": { "A": 1.333e8, "C": 4430.0 } + } + }, + { + "__comment": "Kw: 2 H2O <-> H+ + OH-", + "type": "DISSOLVED_EQUILIBRIUM", + "condensed phase": "AQUEOUS", + "solvent": "H2O", + "reactants": [ + { "species": "H2O", "coefficient": 2 } + ], + "products": [ + { "species": "Hp", "coefficient": 1 }, + { "species": "OHm", "coefficient": 1 } + ], + "algebraic species": "OHm", + "equilibrium constant": { + "A": 3.24e-18, + "C": 0.0 + } + }, + { + "__comment": "Ka1: SO2(aq) <-> H+ + HSO3-", + "type": "DISSOLVED_EQUILIBRIUM", + "condensed phase": "AQUEOUS", + "solvent": "H2O", + "reactants": [ + { "species": "SO2", "coefficient": 1 } + ], + "products": [ + { "species": "Hp", "coefficient": 1 }, + { "species": "HSO3m", "coefficient": 1 } + ], + "algebraic species": "HSO3m", + "equilibrium constant": { + "A": 3.06e-4, + "C": 2090.0 + } + }, + { + "__comment": "Ka2: HSO3- <-> H+ + SO3--", + "type": "DISSOLVED_EQUILIBRIUM", + "condensed phase": "AQUEOUS", + "solvent": "H2O", + "reactants": [ + { "species": "HSO3m", "coefficient": 1 } + ], + "products": [ + { "species": "Hp", "coefficient": 1 }, + { "species": "SO3mm", "coefficient": 1 } + ], + "algebraic species": "SO3mm", + "equilibrium constant": { + "A": 1.08e-9, + "C": 1120.0 + } + }, + { + "__comment": "Mass conservation: total sulfur in reversible pool (includes SO2OOH-)", + "type": "LINEAR_CONSTRAINT", + "algebraic phase": "gas", + "algebraic species": "SO2", + "diagnose from state": true, + "terms": [ + { "phase": "gas", "species": "SO2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "SO2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "HSO3m", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "SO3mm", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "SO2OOHm", "coefficient": 1.0 } + ] + }, + { + "__comment": "Mass conservation: total H2O2", + "type": "LINEAR_CONSTRAINT", + "algebraic phase": "gas", + "algebraic species": "H2O2", + "diagnose from state": true, + "terms": [ + { "phase": "gas", "species": "H2O2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "H2O2", "coefficient": 1.0 } + ] + }, + { + "__comment": "Mass conservation: total O3", + "type": "LINEAR_CONSTRAINT", + "algebraic phase": "gas", + "algebraic species": "O3", + "diagnose from state": true, + "terms": [ + { "phase": "gas", "species": "O3", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "O3", "coefficient": 1.0 } + ] + }, + { + "__comment": "Charge balance: H+ = OH- + HSO3- + 2 SO3-- + 2 SO4-- + SO2OOH-", + "type": "LINEAR_CONSTRAINT", + "algebraic phase": "AQUEOUS", + "algebraic species": "Hp", + "constant [mol m-3]": 0.0, + "terms": [ + { "phase": "AQUEOUS", "species": "Hp", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "species": "OHm", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "species": "HSO3m", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "species": "SO3mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "species": "SO4mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "species": "SO2OOHm", "coefficient": -1.0 } + ] + } + ] +} From d8a9d51eebd40cf05e754bbe4f360bc9705eeaa1 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Tue, 23 Jun 2026 14:05:40 -0600 Subject: [PATCH 02/23] distinguish between micm arrhenius and miam arrhneius --- .../cam_clound_chemistry.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json index f6d7c881..b2699237 100644 --- a/test/integration/integration_configs/cam_clound_chemistry.json +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -56,7 +56,8 @@ "solvent": "H2O", "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.214e-2, - "C [K]": 3120.0 + "C [K]": 3120.0, + "T0 [K]": 298.15 } }, { @@ -69,7 +70,8 @@ "solvent": "H2O", "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 7.306e2, - "C [K]": 6621.0 + "C [K]": 6621.0, + "T0 [K]": 298.15 } }, { @@ -82,7 +84,8 @@ "solvent": "H2O", "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.135e-4, - "C [K]": 2560.0 + "C [K]": 2560.0, + "T0 [K]": 298.15 } }, { @@ -100,10 +103,10 @@ { "species": "H2O", "coefficient": 1 } ], "forward rate constants": { - "CLOUD": { "A": 3.184e8, "C": 4430.0 } + "CLOUD": { "type": "ARRHENIUS_REFERENCE_T", "A": 3.184e8, "C [K]": 4430.0, "T0 [K]": 298.15 } }, "equilibrium constant": { - "A": 1725.0 + "type": "ARRHENIUS_REFERENCE_T", "A": 1725.0, "C [K]": 0.0, "T0 [K]": 298.15 } }, { @@ -120,7 +123,7 @@ { "species name": "SO4mm", "coefficient": 1 } ], "rate constants": { - "CLOUD": { "A": 1.333e8, "C": 4430.0 } + "CLOUD": { "type": "ARRHENIUS_REFERENCE_T", "A": 1.333e8, "C [K]": 4430.0, "T0 [K]": 298.15 } } }, { @@ -137,8 +140,7 @@ ], "algebraic species": "OHm", "equilibrium constant": { - "A": 3.24e-18, - "C": 0.0 + "type": "ARRHENIUS_REFERENCE_T", "A": 3.24e-18, "C [K]": 0.0, "T0 [K]": 298.15 } }, { @@ -155,8 +157,7 @@ ], "algebraic species": "HSO3m", "equilibrium constant": { - "A": 3.06e-4, - "C": 2090.0 + "type": "ARRHENIUS_REFERENCE_T", "A": 3.06e-4, "C [K]": 2090.0, "T0 [K]": 298.15 } }, { @@ -173,8 +174,7 @@ ], "algebraic species": "SO3mm", "equilibrium constant": { - "A": 1.08e-9, - "C": 1120.0 + "type": "ARRHENIUS_REFERENCE_T", "A": 1.08e-9, "C [K]": 1120.0, "T0 [K]": 298.15 } }, { From e6a6f305245918bc01700768ce659fd8545d090c Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Tue, 23 Jun 2026 19:14:02 -0600 Subject: [PATCH 03/23] add miam struct and keys --- .../mechanism_configuration/aerosol_types.hpp | 167 ++++++++++++++++++ src/detail/v1/aerosol_keys.hpp | 90 ++++++++++ .../cam_clound_chemistry.json | 14 +- 3 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 include/mechanism_configuration/aerosol_types.hpp create mode 100644 src/detail/v1/aerosol_keys.hpp diff --git a/include/mechanism_configuration/aerosol_types.hpp b/include/mechanism_configuration/aerosol_types.hpp new file mode 100644 index 00000000..a10cb2f3 --- /dev/null +++ b/include/mechanism_configuration/aerosol_types.hpp @@ -0,0 +1,167 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace mechanism_configuration::types +{ + // ---------------------------------------- + // Rate constants + // ---------------------------------------- + + /// @brief Reference-temperature Arrhenius / van 't Hoff form: + /// f(T) = A * exp( C * (1/T0 - 1/T) ) (C = +Ea/R, positive) + struct ArrheniusReferenceTemperature + { + double A; ///< Value at the reference temperature T0 [units vary by use] + double C = 0.0; ///< Temperature-dependence parameter [K] (C = +Ea/R) + double T0 = 298.15; ///< Reference temperature [K] + }; + + /// @brief Equilibrium constants use the same reference-temperature form. + using EquilibriumConstant = ArrheniusReferenceTemperature; + + /// @brief Henry's law constant: HLC(T) = HLC_ref * exp( C * (1/T - 1/T0) ) + /// Same van 't Hoff kernel as ArrheniusReferenceTemperature but with the + /// opposite temperature trend (solubility rises as T falls) + struct HenryLawConstant + { + double HLC_ref; ///< Reference HLC at T0 [mol m-3 Pa-1] + double C = 0.0; ///< Temperature-dependence parameter [K] + double T0 = 298.15; ///< Reference temperature [K] + }; + + /// @brief A reaction rate constant parsed from config. + using RateConstant = std::variant>; + + // ---------------------------------------- + // Representations + // ---------------------------------------- + + struct UniformSection + { + std::string name; + std::vector phases; + double min_radius; ///< Minimum section radius [m] + double max_radius; ///< Maximum section radius [m] + }; + + struct SingleMomentMode + { + std::string name; + std::vector phases; + double geometric_mean_radius; ///< Geometric mean radius [m] + double geometric_standard_deviation; ///< Geometric standard deviation [-] + }; + + struct TwoMomentMode + { + std::string name; + std::vector phases; + double geometric_standard_deviation; ///< Geometric standard deviation [-] + }; + + using Representation = std::variant; + + // ---------------------------------------- + // Processes + // ---------------------------------------- + + struct DissolvedReaction + { + std::string phase; + std::string solvent; + std::vector reactants; + std::vector products; + /// @brief Rate constant per aerosol representation; keys are representation names. + std::map rate_constants; + }; + + struct DissolvedReversibleReaction + { + std::string phase; + std::string solvent; ///< Solvent species name (config key: "solvent") + std::vector reactants; + std::vector products; + /// @brief Per-representation forward / reverse rate constants; keys are representation names. + /// Supply exactly two of {forward, reverse, equilibrium} per representation; the third is derived. + std::map forward_rate_constants; + std::map reverse_rate_constants; + /// @brief Shared, intrinsic equilibrium constant (NOT per representation). + std::optional equilibrium_constant; + }; + + struct HenryLawPhaseTransfer + { + std::string gas_phase; + std::string gas_species; + std::string condensed_phase; + std::string condensed_species; + std::string solvent; + HenryLawConstant henry_law_constant; + HenryLawConstant henry_law_constant; + double diffusion_coefficient; ///< Gas-phase diffusion coefficient [m2 s-1] + double accommodation_coefficient; ///< Mass accommodation coefficient, dimensionless + }; + + using Process = std::variant; + + // ---------------------------------------- + // Constraints + // ---------------------------------------- + + struct HenryLawEquilibrium + { + std::string gas_phase; + std::string gas_species; + std::string condensed_phase; + std::string condensed_species; + std::string solvent; + HenryLawConstant henry_law_constant; + double solvent_molecular_weight; ///< Solvent molecular weight [kg mol-1] + double solvent_density; ///< Solvent density [kg m-3] + }; + + struct DissolvedEquilibrium + { + std::string phase; + std::string algebraic_species; + std::string solvent; + std::vector reactants; + std::vector products; + EquilibriumConstant equilibrium_constant; + }; + + struct LinearConstraintTerm + { + std::string phase; + std::string species; + double coefficient; + }; + + /// @brief RHS constant C of a LinearConstraint: G = sum(coeff_i * [species_i]) - C = 0 + /// Choose exactly one mode (mutually exclusive): + struct FixedConstant { double value = 0.0; }; ///< Fixed value [mol m-3], shared by all instances + struct DiagnoseFromState {}; ///< Computed per representation instance + + struct LinearConstraint + { + std::string algebraic_phase; + std::string algebraic_species; + std::vector terms; + std::variant constant = FixedConstant{ 0.0 }; + }; + + using Constraint = std::variant; + +} // namespace mechanism_configuration::types \ No newline at end of file diff --git a/src/detail/v1/aerosol_keys.hpp b/src/detail/v1/aerosol_keys.hpp new file mode 100644 index 00000000..c93825e3 --- /dev/null +++ b/src/detail/v1/aerosol_keys.hpp @@ -0,0 +1,90 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace mechanism_configuration::v1::keys +{ + // ---------------------------------------- + // Top-level containers + // ---------------------------------------- + inline constexpr std::string_view aerosol_representations = "aerosol representations"; + inline constexpr std::string_view aerosol_processes = "aerosol processes"; + + // ---------------------------------------- + // Representations + // ---------------------------------------- + // also: name, phases + + inline constexpr std::string_view UniformSection_key = "UNIFORM_SECTION"; + inline constexpr std::string_view minimum_radius = "minimum radius [m]"; + inline constexpr std::string_view maximum_radius = "maximum radius [m]"; + + inline constexpr std::string_view SingleMomentMode_key = "SINGLE_MOMENT_MODE"; + inline constexpr std::string_view geometric_mean_radius = "geometric mean radius [m]"; + inline constexpr std::string_view geometric_standard_deviation = "geometric standard deviation [-]"; + + inline constexpr std::string_view TwoMomentMode_key = "TWO_MOMENT_MODE"; + // also: geometric_standard_deviation + + // ---------------------------------------- + // Rate constants + // ---------------------------------------- + inline constexpr std::string_view rate_constants = "rate constants"; + inline constexpr std::string_view forward_rate_constants = "forward rate constants"; + inline constexpr std::string_view reverse_rate_constants = "reverse rate constants"; + inline constexpr std::string_view equilibrium_constant = "equilibrium constant"; + inline constexpr std::string_view reference_temperature = "T0 [K]"; + + inline constexpr std::string_view henry_law_constant = "Henry's law constant"; + inline constexpr std::string_view HLC_ref = "HLC_ref [mol m-3 Pa-1]"; + inline constexpr std::string_view henry_law_C = "C [K]"; + + // ---------------------------------------- + // Processes + // ---------------------------------------- + inline constexpr std::string_view solvent = "solvent"; + + // HenryLawPhaseTransfer + // also: gas_phase, gas_phase_species, condensed_phase, condensed_phase_species, solvent, + // henry_law_constant, diffusion_coefficient + inline constexpr std::string_view HenryLawPhaseTransfer_key = "HENRY_LAW_PHASE_TRANSFER"; + inline constexpr std::string_view accommodation_coefficient = "accommodation coefficient [-]"; + + // DissolvedReaction + // also: condensed_phase, solvent, reactants, products, rate_constants + inline constexpr std::string_view DissolvedReaction_key = "DISSOLVED_REACTION"; + + // DissolvedReversibleReaction + // also: condensed_phase, solvent, reactants, products, + // forward_rate_constants, reverse_rate_constants, equilibrium_constant + inline constexpr std::string_view DissolvedReversibleReaction_key = "DISSOLVED_REVERSIBLE_REACTION"; + + // ---------------------------------------- + // Constraints + // ---------------------------------------- + // HenryLawEquilibrium + // also: gas_phase, gas_phase_species, condensed_phase, condensed_phase_species, solvent, + // henry_law_constant + inline constexpr std::string_view HenryLawEquilibrium_key = "HENRY_LAW_EQUILIBRIUM"; + inline constexpr std::string_view solvent_molecular_weight = "solvent molecular weight [kg mol-1]"; + inline constexpr std::string_view solvent_density = "solvent density [kg m-3]"; + + // DissolvedEquilibrium + // also: condensed_phase, solvent, reactants, products, equilibrium_constant + inline constexpr std::string_view DissolvedEquilibrium_key = "DISSOLVED_EQUILIBRIUM"; + inline constexpr std::string_view algebraic_species = "algebraic species"; + + // LinearConstraint + // also: name; terms carry coefficient and species + inline constexpr std::string_view LinearConstraint_key = "LINEAR_CONSTRAINT"; + inline constexpr std::string_view algebraic_phase = "algebraic phase"; + inline constexpr std::string_view terms = "terms"; + inline constexpr std::string_view phase = "phase"; + inline constexpr std::string_view constant = "constant [mol m-3]"; + inline constexpr std::string_view diagnose_from_state = "diagnose from state"; + +} // namespace mechanism_configuration::v1::keys diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json index b2699237..e35a8430 100644 --- a/test/integration/integration_configs/cam_clound_chemistry.json +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -103,10 +103,10 @@ { "species": "H2O", "coefficient": 1 } ], "forward rate constants": { - "CLOUD": { "type": "ARRHENIUS_REFERENCE_T", "A": 3.184e8, "C [K]": 4430.0, "T0 [K]": 298.15 } + "CLOUD": { "type": "ARRHENIUS", "A": 3.184e8, "C": 4430.0 } }, "equilibrium constant": { - "type": "ARRHENIUS_REFERENCE_T", "A": 1725.0, "C [K]": 0.0, "T0 [K]": 298.15 + "type": "ARRHENIUS_REFERENCE_TEMPERATURE", "A": 1725.0, "C [K]": 0.0, "T0 [K]": 298.15 } }, { @@ -123,7 +123,7 @@ { "species name": "SO4mm", "coefficient": 1 } ], "rate constants": { - "CLOUD": { "type": "ARRHENIUS_REFERENCE_T", "A": 1.333e8, "C [K]": 4430.0, "T0 [K]": 298.15 } + "CLOUD": { "type": "ARRHENIUS", "A": 1.333e8, "C": 4430.0} } }, { @@ -140,7 +140,7 @@ ], "algebraic species": "OHm", "equilibrium constant": { - "type": "ARRHENIUS_REFERENCE_T", "A": 3.24e-18, "C [K]": 0.0, "T0 [K]": 298.15 + "type": "ARRHENIUS_REFERENCE_TEMPERATURE", "A": 3.24e-18, "C [K]": 0.0, "T0 [K]": 298.15 } }, { @@ -157,13 +157,14 @@ ], "algebraic species": "HSO3m", "equilibrium constant": { - "type": "ARRHENIUS_REFERENCE_T", "A": 3.06e-4, "C [K]": 2090.0, "T0 [K]": 298.15 + "type": "ARRHENIUS_REFERENCE_TEMPERATURE", "A": 3.06e-4, "C [K]": 2090.0, "T0 [K]": 298.15 } }, { "__comment": "Ka2: HSO3- <-> H+ + SO3--", "type": "DISSOLVED_EQUILIBRIUM", "condensed phase": "AQUEOUS", + "algebraic species": "SO3mm", "solvent": "H2O", "reactants": [ { "species": "HSO3m", "coefficient": 1 } @@ -172,9 +173,8 @@ { "species": "Hp", "coefficient": 1 }, { "species": "SO3mm", "coefficient": 1 } ], - "algebraic species": "SO3mm", "equilibrium constant": { - "type": "ARRHENIUS_REFERENCE_T", "A": 1.08e-9, "C [K]": 1120.0, "T0 [K]": 298.15 + "type": "ARRHENIUS_REFERENCE_TEMPERATURE", "A": 1.08e-9, "C [K]": 1120.0, "T0 [K]": 298.15 } }, { From 5bee7cbf9fee1cfac913efdb21407ba209c2ca3f Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Tue, 23 Jun 2026 19:22:04 -0600 Subject: [PATCH 04/23] code cleanup --- .../mechanism_configuration/aerosol_types.hpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/include/mechanism_configuration/aerosol_types.hpp b/include/mechanism_configuration/aerosol_types.hpp index a10cb2f3..e88c096a 100644 --- a/include/mechanism_configuration/aerosol_types.hpp +++ b/include/mechanism_configuration/aerosol_types.hpp @@ -19,7 +19,7 @@ namespace mechanism_configuration::types // Rate constants // ---------------------------------------- - /// @brief Reference-temperature Arrhenius / van 't Hoff form: + /// @brief Reference-temperature Arrhenius /// f(T) = A * exp( C * (1/T0 - 1/T) ) (C = +Ea/R, positive) struct ArrheniusReferenceTemperature { @@ -32,7 +32,7 @@ namespace mechanism_configuration::types using EquilibriumConstant = ArrheniusReferenceTemperature; /// @brief Henry's law constant: HLC(T) = HLC_ref * exp( C * (1/T - 1/T0) ) - /// Same van 't Hoff kernel as ArrheniusReferenceTemperature but with the + /// Same as ArrheniusReferenceTemperature but with the /// opposite temperature trend (solubility rises as T falls) struct HenryLawConstant { @@ -52,23 +52,23 @@ namespace mechanism_configuration::types { std::string name; std::vector phases; - double min_radius; ///< Minimum section radius [m] - double max_radius; ///< Maximum section radius [m] + double min_radius; ///< [m] + double max_radius; ///< [m] }; struct SingleMomentMode { std::string name; std::vector phases; - double geometric_mean_radius; ///< Geometric mean radius [m] - double geometric_standard_deviation; ///< Geometric standard deviation [-] + double geometric_mean_radius; ///< [m] + double geometric_standard_deviation; ///< dimensionless }; struct TwoMomentMode { std::string name; std::vector phases; - double geometric_standard_deviation; ///< Geometric standard deviation [-] + double geometric_standard_deviation; ///< dimensionless }; using Representation = std::variant; @@ -90,7 +90,7 @@ namespace mechanism_configuration::types struct DissolvedReversibleReaction { std::string phase; - std::string solvent; ///< Solvent species name (config key: "solvent") + std::string solvent; std::vector reactants; std::vector products; /// @brief Per-representation forward / reverse rate constants; keys are representation names. @@ -128,8 +128,8 @@ namespace mechanism_configuration::types std::string condensed_species; std::string solvent; HenryLawConstant henry_law_constant; - double solvent_molecular_weight; ///< Solvent molecular weight [kg mol-1] - double solvent_density; ///< Solvent density [kg m-3] + double solvent_molecular_weight; ///< [kg mol-1] + double solvent_density; ///< [kg m-3] }; struct DissolvedEquilibrium From d2e6a89a0f8691d4912fe113caba15516706e067 Mon Sep 17 00:00:00 2001 From: Jiwon Gim <55209567+boulderdaze@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:12:57 -0600 Subject: [PATCH 05/23] Update test/integration/integration_configs/cam_clound_chemistry.json Co-authored-by: Kyle Shores --- test/integration/integration_configs/cam_clound_chemistry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json index e35a8430..829221b4 100644 --- a/test/integration/integration_configs/cam_clound_chemistry.json +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "1.2.0", "name": "CAM Cloud Chemistry", "species": [ From 8f578046728b1c1aff5ebcb0dab8f6d5b078958f Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Wed, 24 Jun 2026 09:13:30 -0600 Subject: [PATCH 06/23] add headers --- include/mechanism_configuration/mechanism_configuration.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/mechanism_configuration/mechanism_configuration.hpp b/include/mechanism_configuration/mechanism_configuration.hpp index db2e42a5..c949e999 100644 --- a/include/mechanism_configuration/mechanism_configuration.hpp +++ b/include/mechanism_configuration/mechanism_configuration.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include From 557d8b27f9becfdd18deac87f77c39b9a7949dca Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Wed, 24 Jun 2026 17:26:55 -0600 Subject: [PATCH 07/23] Add parsers --- .../mechanism_configuration/aerosol_types.hpp | 23 +- include/mechanism_configuration/mechanism.hpp | 5 +- src/detail/v1/aerosol_type_parsers.hpp | 63 ++++ src/detail/v1/aerosol_type_schema.hpp | 24 ++ src/v1/CMakeLists.txt | 2 + src/v1/aerosol_type_parsers.cpp | 273 +++++++++++++++++ src/v1/aerosol_type_schema.cpp | 280 ++++++++++++++++++ 7 files changed, 660 insertions(+), 10 deletions(-) create mode 100644 src/detail/v1/aerosol_type_parsers.hpp create mode 100644 src/detail/v1/aerosol_type_schema.hpp create mode 100644 src/v1/aerosol_type_parsers.cpp create mode 100644 src/v1/aerosol_type_schema.cpp diff --git a/include/mechanism_configuration/aerosol_types.hpp b/include/mechanism_configuration/aerosol_types.hpp index fcc0e25a..13017b78 100644 --- a/include/mechanism_configuration/aerosol_types.hpp +++ b/include/mechanism_configuration/aerosol_types.hpp @@ -28,9 +28,6 @@ namespace mechanism_configuration::types double T0 = 298.15; ///< Reference temperature [K] }; - /// @brief Equilibrium constants use the same reference-temperature form. - using EquilibriumConstant = ArrheniusReferenceTemperature; - /// @brief Henry's law constant: HLC(T) = HLC_ref * exp( C * (1/T - 1/T0) ) /// Same as ArrheniusReferenceTemperature but with the /// opposite temperature trend (solubility rises as T falls) @@ -42,7 +39,8 @@ namespace mechanism_configuration::types }; /// @brief A reaction rate constant parsed from config. - using RateConstant = std::variant>; + // using RateConstant = std::variant>; // TODO + using RateConstant = std::variant>; // ---------------------------------------- // Representations @@ -98,7 +96,7 @@ namespace mechanism_configuration::types std::map forward_rate_constants; std::map reverse_rate_constants; /// @brief Shared, intrinsic equilibrium constant (NOT per representation). - std::optional equilibrium_constant; + std::optional equilibrium_constant; }; struct HenryLawPhaseTransfer @@ -109,7 +107,6 @@ namespace mechanism_configuration::types std::string condensed_species; std::string solvent; HenryLawConstant henry_law_constant; - HenryLawConstant henry_law_constant; double diffusion_coefficient; ///< Gas-phase diffusion coefficient [m2 s-1] double accommodation_coefficient; ///< Mass accommodation coefficient, dimensionless }; @@ -139,7 +136,7 @@ namespace mechanism_configuration::types std::string solvent; std::vector reactants; std::vector products; - EquilibriumConstant equilibrium_constant; + ArrheniusReferenceTemperature equilibrium_constant; }; struct LinearConstraintTerm @@ -169,4 +166,16 @@ namespace mechanism_configuration::types using Constraint = std::variant; + // ---------------------------------------- + // Container + // ---------------------------------------- + + /// @brief Collection of parsed aerosol entries + struct Aerosol + { + std::vector representations; + std::vector processes; + std::vector constraints; + }; + } // namespace mechanism_configuration::types \ No newline at end of file diff --git a/include/mechanism_configuration/mechanism.hpp b/include/mechanism_configuration/mechanism.hpp index 2c656adc..d6201ad2 100644 --- a/include/mechanism_configuration/mechanism.hpp +++ b/include/mechanism_configuration/mechanism.hpp @@ -13,9 +13,8 @@ namespace mechanism_configuration { - // Trying to build on linux for the python release, I learned that glibc had a bug which defined -// a macro called major and minor. This caused a conflict with the Version struct. To fix this, I -// undefine the macros before defining the struct and then redefine them after the struct. +// glibc defines `major` and `minor` as macros, which conflict with the `Version` struct fields. +// Temporarily undefine the macros before the struct definition and restore them afterward. // https://stackoverflow.com/a/22253389/5217293 #pragma push_macro("major") #undef major diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp new file mode 100644 index 00000000..033cc585 --- /dev/null +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -0,0 +1,63 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#include + +#include +#include +#include + +// Each Parse function assumes its input has already been validated by the matching CheckAerosol*Schema. +namespace mechanism_configuration::v1 +{ + // ---------------------------------------- + // Rate constant parsers + // ---------------------------------------- + + types::HenryLawConstant ParseHenryLawConstant(const YAML::Node& object); + types::ArrheniusReferenceTemperature ParseArrheniusReferenceTemperature(const YAML::Node& object); + + /// @brief Parses a per-representation rate-constant map, e.g. { "CLOUD": { ... } }. + /// @return Map keyed by representation name to its rate constant. + std::map ParseRateConstantMap(const YAML::Node& object); + + // ---------------------------------------- + // Representation parsers + // ---------------------------------------- + + types::UniformSection ParseUniformSection(const YAML::Node& object); + types::SingleMomentMode ParseSingleMomentMode(const YAML::Node& object); + types::TwoMomentMode ParseTwoMomentMode(const YAML::Node& object); + + // ---------------------------------------- + // Process parsers + // ---------------------------------------- + + types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer(const YAML::Node& object); + types::DissolvedReaction ParseDissolvedReaction(const YAML::Node& object); + types::DissolvedReversibleReaction ParseDissolvedReversibleReaction(const YAML::Node& object); + + // ---------------------------------------- + // Constraint parsers + // ---------------------------------------- + + types::HenryLawEquilibrium ParseHenryLawEquilibrium(const YAML::Node& object); + types::DissolvedEquilibrium ParseDissolvedEquilibrium(const YAML::Node& object); + types::LinearConstraint ParseLinearConstraint(const YAML::Node& object); + + // ---------------------------------------- + // Container parsers + // ---------------------------------------- + + std::vector ParseAerosolRepresentations(const YAML::Node& objects); + + /// @brief Parses the aerosol processes sequence into the processes or constraints + void ParseAerosolProcesses(const YAML::Node& objects, types::Aerosol& aerosol); + +} // namespace mechanism_configuration::v1 diff --git a/src/detail/v1/aerosol_type_schema.hpp b/src/detail/v1/aerosol_type_schema.hpp new file mode 100644 index 00000000..331ef536 --- /dev/null +++ b/src/detail/v1/aerosol_type_schema.hpp @@ -0,0 +1,24 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include + +namespace mechanism_configuration::v1 +{ + /// @brief Schema-validates each entry of the aerosol representations section. + /// @param representations_list YAML node containing the representation entries + /// @return List of structural errors, or empty if all entries conform + Errors CheckAerosolRepresentationsSchema(const YAML::Node& representations_list); + + /// @brief Schema-validates each entry of the aerosol processes section which mixes + /// process and constraint types. + /// @param processes_list YAML node containing the process/constraint entries + /// @return List of structural errors, or empty if all entries conform + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list); + +} // namespace mechanism_configuration::v1 diff --git a/src/v1/CMakeLists.txt b/src/v1/CMakeLists.txt index d752c785..014c6a63 100644 --- a/src/v1/CMakeLists.txt +++ b/src/v1/CMakeLists.txt @@ -4,6 +4,8 @@ target_sources(mechanism_configuration type_parsers.cpp type_schema.cpp utils.cpp + aerosol_type_parsers.cpp + aerosol_type_schema.cpp ) add_subdirectory(reactions) \ No newline at end of file diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp new file mode 100644 index 00000000..d0eb48b5 --- /dev/null +++ b/src/v1/aerosol_type_parsers.cpp @@ -0,0 +1,273 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#include "detail/v1/aerosol_type_parsers.hpp" + +#include "detail/v1/aerosol_keys.hpp" +#include "detail/v1/keys.hpp" +#include "detail/v1/type_parsers.hpp" + +namespace mechanism_configuration +{ + namespace v1 + { + // ---------------------------------------- + // Rate constants parser + // ---------------------------------------- + + types::ArrheniusReferenceTemperature ParseArrheniusReferenceTemperature(const YAML::Node& object) + { + types::ArrheniusReferenceTemperature rate_constant; + + rate_constant.A = object[keys::A].as(); + if (object[keys::C]) + rate_constant.C = object[keys::C].as(); + if (object[keys::reference_temperature]) + rate_constant.T0 = object[keys::reference_temperature].as(); + + return rate_constant; + } + + types::HenryLawConstant ParseHenryLawConstant(const YAML::Node& object) + { + types::HenryLawConstant henry_law_constant; + + henry_law_constant.HLC_ref = object[keys::HLC_ref].as(); + if (object[keys::henry_law_C]) + henry_law_constant.C = object[keys::henry_law_C].as(); + if (object[keys::reference_temperature]) + henry_law_constant.T0 = object[keys::reference_temperature].as(); + + return henry_law_constant; + } + + std::map ParseRateConstantMap(const YAML::Node& object) + { + std::map rate_constants; + + // Each key is an aerosol representation name (e.g. "CLOUD"); each value is a + // reference-temperature Arrhenius block. + for (const auto& entry : object) + { + rate_constants.emplace(entry.first.as(), ParseArrheniusReferenceTemperature(entry.second)); + } + + return rate_constants; + } + + // ---------------------------------------- + // Representation parsers + // ---------------------------------------- + + namespace + { + std::vector ParsePhaseNames(const YAML::Node& object) + { + std::vector phases; + for (const auto& phase : object[keys::phases]) + phases.push_back(phase.as()); + return phases; + } + } // namespace + + types::UniformSection ParseUniformSection(const YAML::Node& object) + { + types::UniformSection uniform_section; + + uniform_section.name = object[keys::name].as(); + uniform_section.phases = ParsePhaseNames(object); + uniform_section.min_radius = object[keys::minimum_radius].as(); + uniform_section.max_radius = object[keys::maximum_radius].as(); + + return uniform_section; + } + + types::SingleMomentMode ParseSingleMomentMode(const YAML::Node& object) + { + types::SingleMomentMode single_moment_mode; + + single_moment_mode.name = object[keys::name].as(); + single_moment_mode.phases = ParsePhaseNames(object); + single_moment_mode.geometric_mean_radius = object[keys::geometric_mean_radius].as(); + single_moment_mode.geometric_standard_deviation = object[keys::geometric_standard_deviation].as(); + + return single_moment_mode; + } + + types::TwoMomentMode ParseTwoMomentMode(const YAML::Node& object) + { + types::TwoMomentMode two_moment_mode; + + two_moment_mode.name = object[keys::name].as(); + two_moment_mode.phases = ParsePhaseNames(object); + two_moment_mode.geometric_standard_deviation = object[keys::geometric_standard_deviation].as(); + + return two_moment_mode; + } + + // ---------------------------------------- + // Process parsers + // ---------------------------------------- + + types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer(const YAML::Node& object) + { + types::HenryLawPhaseTransfer transfer; + + transfer.gas_phase = object[keys::gas_phase].as(); + transfer.gas_species = object[keys::gas_phase_species].as(); + transfer.condensed_phase = object[keys::condensed_phase].as(); + transfer.condensed_species = object[keys::condensed_phase_species].as(); + transfer.solvent = object[keys::solvent].as(); + transfer.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); + transfer.diffusion_coefficient = object[keys::diffusion_coefficient].as(); + transfer.accommodation_coefficient = object[keys::accommodation_coefficient].as(); + + return transfer; + } + + types::DissolvedReaction ParseDissolvedReaction(const YAML::Node& object) + { + types::DissolvedReaction reaction; + + reaction.phase = object[keys::condensed_phase].as(); + reaction.solvent = object[keys::solvent].as(); + reaction.reactants = ParseReactionComponents(object, keys::reactants); + reaction.products = ParseReactionComponents(object, keys::products); + reaction.rate_constants = ParseRateConstantMap(object[keys::rate_constants]); + + return reaction; + } + + types::DissolvedReversibleReaction ParseDissolvedReversibleReaction(const YAML::Node& object) + { + types::DissolvedReversibleReaction reaction; + + reaction.phase = object[keys::condensed_phase].as(); + reaction.solvent = object[keys::solvent].as(); + reaction.reactants = ParseReactionComponents(object, keys::reactants); + reaction.products = ParseReactionComponents(object, keys::products); + + // Any two of {forward, reverse, equilibrium} may be supplied; the third is derived + // downstream, so each is parsed only when present. + if (object[keys::forward_rate_constants]) + reaction.forward_rate_constants = ParseRateConstantMap(object[keys::forward_rate_constants]); + if (object[keys::reverse_rate_constants]) + reaction.reverse_rate_constants = ParseRateConstantMap(object[keys::reverse_rate_constants]); + if (object[keys::equilibrium_constant]) + reaction.equilibrium_constant = ParseArrheniusReferenceTemperature(object[keys::equilibrium_constant]); + + return reaction; + } + + // ---------------------------------------- + // Constraint parsers + // ---------------------------------------- + + types::HenryLawEquilibrium ParseHenryLawEquilibrium(const YAML::Node& object) + { + types::HenryLawEquilibrium equilibrium; + + equilibrium.gas_phase = object[keys::gas_phase].as(); + equilibrium.gas_species = object[keys::gas_phase_species].as(); + equilibrium.condensed_phase = object[keys::condensed_phase].as(); + equilibrium.condensed_species = object[keys::condensed_phase_species].as(); + equilibrium.solvent = object[keys::solvent].as(); + equilibrium.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); + + // Solvent properties are optional here; when omitted they are sourced from the solvent + // species definition downstream. + equilibrium.solvent_molecular_weight = + object[keys::solvent_molecular_weight] ? object[keys::solvent_molecular_weight].as() : 0.0; + equilibrium.solvent_density = object[keys::solvent_density] ? object[keys::solvent_density].as() : 0.0; + + return equilibrium; + } + + types::DissolvedEquilibrium ParseDissolvedEquilibrium(const YAML::Node& object) + { + types::DissolvedEquilibrium equilibrium; + + equilibrium.phase = object[keys::condensed_phase].as(); + equilibrium.algebraic_species = object[keys::algebraic_species].as(); + equilibrium.solvent = object[keys::solvent].as(); + equilibrium.reactants = ParseReactionComponents(object, keys::reactants); + equilibrium.products = ParseReactionComponents(object, keys::products); + equilibrium.equilibrium_constant = ParseArrheniusReferenceTemperature(object[keys::equilibrium_constant]); + + return equilibrium; + } + + types::LinearConstraint ParseLinearConstraint(const YAML::Node& object) + { + types::LinearConstraint constraint; + + constraint.algebraic_phase = object[keys::algebraic_phase].as(); + constraint.algebraic_species = object[keys::algebraic_species].as(); + + for (const auto& term_node : object[keys::terms]) + { + types::LinearConstraintTerm term; + term.phase = term_node[keys::phase].as(); + term.species = term_node[keys::species].as(); + term.coefficient = term_node[keys::coefficient].as(); + constraint.terms.push_back(term); + } + + // RHS constant: diagnose-from-state, else a fixed value, else the default FixedConstant{0}. + if (object[keys::diagnose_from_state] && object[keys::diagnose_from_state].as()) + constraint.constant = types::DiagnoseFromState{}; + else if (object[keys::constant]) + constraint.constant = types::FixedConstant{ object[keys::constant].as() }; + + return constraint; + } + + // ---------------------------------------- + // Container parsers + // ---------------------------------------- + + std::vector ParseAerosolRepresentations(const YAML::Node& objects) + { + std::vector representations; + + for (const auto& object : objects) + { + const auto type = object[keys::type].as(); + + if (type == keys::UniformSection_key) + representations.emplace_back(ParseUniformSection(object)); + else if (type == keys::SingleMomentMode_key) + representations.emplace_back(ParseSingleMomentMode(object)); + else if (type == keys::TwoMomentMode_key) + representations.emplace_back(ParseTwoMomentMode(object)); + } + + return representations; + } + + void ParseAerosolProcesses(const YAML::Node& objects, types::Aerosol& aerosol) + { + for (const auto& object : objects) + { + const auto type = object[keys::type].as(); + + // Processes + if (type == keys::HenryLawPhaseTransfer_key) + aerosol.processes.emplace_back(ParseHenryLawPhaseTransfer(object)); + else if (type == keys::DissolvedReaction_key) + aerosol.processes.emplace_back(ParseDissolvedReaction(object)); + else if (type == keys::DissolvedReversibleReaction_key) + aerosol.processes.emplace_back(ParseDissolvedReversibleReaction(object)); + // Constraints + else if (type == keys::HenryLawEquilibrium_key) + aerosol.constraints.emplace_back(ParseHenryLawEquilibrium(object)); + else if (type == keys::DissolvedEquilibrium_key) + aerosol.constraints.emplace_back(ParseDissolvedEquilibrium(object)); + else if (type == keys::LinearConstraint_key) + aerosol.constraints.emplace_back(ParseLinearConstraint(object)); + } + } + + } // namespace v1 +} // namespace mechanism_configuration diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp new file mode 100644 index 00000000..18c59534 --- /dev/null +++ b/src/v1/aerosol_type_schema.cpp @@ -0,0 +1,280 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#include "detail/v1/aerosol_type_schema.hpp" + +#include "detail/check_schema.hpp" +#include "detail/v1/aerosol_keys.hpp" +#include "detail/v1/keys.hpp" +#include "detail/v1/type_schema.hpp" +#include "detail/v1/utils.hpp" + +#include + +#include +#include +#include + +namespace mechanism_configuration +{ + namespace v1 + { + namespace + { + Errors CheckArrheniusReferenceTemperatureSchema(const YAML::Node& object) + { + const std::vector required_keys = { keys::A, keys::C }; + const std::vector optional_keys = { keys::reference_temperature }; + return CheckSchema(object, required_keys, optional_keys); + } + + Errors CheckHenryLawConstantSchema(const YAML::Node& object) + { + const std::vector required_keys = { keys::HLC_ref, keys::henry_law_C }; + const std::vector optional_keys = { keys::reference_temperature }; + return CheckSchema(object, required_keys, optional_keys); + } + + // A per-representation rate-constant map; each value is a reference-temperature block. + Errors CheckRateConstantMapSchema(const YAML::Node& object) + { + Errors errors; + for (const auto& entry : object) + { + auto entry_errors = CheckArrheniusReferenceTemperatureSchema(entry.second); // TODO + errors.insert(errors.end(), entry_errors.begin(), entry_errors.end()); + } + return errors; + } + + // Each linear-constraint term references a species in a phase with a coefficient. + Errors CheckLinearConstraintTermsSchema(const YAML::Node& object) + { + Errors errors; + const std::vector required_keys = { keys::phase, keys::species, keys::coefficient }; + const std::vector optional_keys = {}; + for (const auto& term : object) + { + auto term_errors = CheckSchema(term, required_keys, optional_keys); + errors.insert(errors.end(), term_errors.begin(), term_errors.end()); + } + return errors; + } + } // namespace + + Errors CheckAerosolRepresentationsSchema(const YAML::Node& representations_list) + { + Errors errors; + + for (const auto& object : AsSequence(representations_list)) + { + // Every representation needs a type to dispatch the remaining required keys on. + if (!object[keys::type]) + { + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + errors.push_back( + { ErrorCode::RequiredKeyNotFound, + mc_fmt::format("{} error: Missing 'type' object in aerosol representation.", error_location) }); + continue; + } + + // Keys shared by every representation type. + std::vector required_keys = { keys::type, keys::name, keys::phases }; + std::vector optional_keys = {}; + + const std::string type = object[keys::type].as(); + if (type == keys::UniformSection_key) + { + required_keys.push_back(keys::minimum_radius); + required_keys.push_back(keys::maximum_radius); + } + else if (type == keys::SingleMomentMode_key) + { + required_keys.push_back(keys::geometric_mean_radius); + required_keys.push_back(keys::geometric_standard_deviation); + } + else if (type == keys::TwoMomentMode_key) + { + required_keys.push_back(keys::geometric_standard_deviation); + } + else + { + const auto& node = object[keys::type]; + ErrorLocation error_location{ node.Mark().line, node.Mark().column }; + errors.push_back( + { ErrorCode::UnknownType, + mc_fmt::format("{} error: Unknown aerosol representation type '{}' found.", error_location, type) }); + continue; + } + + auto schema_errors = CheckSchema(object, required_keys, optional_keys); + errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); + } + + return errors; + } + + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list) + { + Errors errors; + + for (const auto& object : AsSequence(processes_list)) + { + // Every entry needs a type to dispatch the remaining required keys on. + if (!object[keys::type]) + { + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + errors.push_back( + { ErrorCode::RequiredKeyNotFound, + mc_fmt::format("{} error: Missing 'type' object in aerosol process.", error_location) }); + continue; + } + + std::vector required_keys; + std::vector optional_keys; + Errors nested_errors; + + const std::string type = object[keys::type].as(); + if (type == keys::HenryLawPhaseTransfer_key) + { + required_keys = { keys::type, + keys::gas_phase, + keys::gas_phase_species, + keys::condensed_phase, + keys::condensed_phase_species, + keys::solvent, + keys::henry_law_constant, + keys::diffusion_coefficient, + keys::accommodation_coefficient }; // TODO + if (object[keys::henry_law_constant]) + nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); + } + else if (type == keys::DissolvedReaction_key) + { + required_keys = { keys::type, keys::condensed_phase, keys::solvent, + keys::reactants, keys::products, keys::rate_constants }; + if (object[keys::reactants]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::reactants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::products]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::products]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::rate_constants]) + { + auto e = CheckRateConstantMapSchema(object[keys::rate_constants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + } + else if (type == keys::DissolvedReversibleReaction_key) + { + required_keys = { keys::type, keys::condensed_phase, keys::solvent, keys::reactants, keys::products }; + optional_keys = { keys::forward_rate_constants, keys::reverse_rate_constants, keys::equilibrium_constant }; + if (object[keys::reactants]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::reactants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::products]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::products]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::forward_rate_constants]) + { + auto e = CheckRateConstantMapSchema(object[keys::forward_rate_constants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::reverse_rate_constants]) + { + auto e = CheckRateConstantMapSchema(object[keys::reverse_rate_constants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::equilibrium_constant]) + { + auto e = CheckArrheniusReferenceTemperatureSchema(object[keys::equilibrium_constant]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + } + else if (type == keys::HenryLawEquilibrium_key) + { + required_keys = { keys::type, + keys::gas_phase, + keys::gas_phase_species, + keys::condensed_phase, + keys::condensed_phase_species, + keys::solvent, + keys::henry_law_constant }; + optional_keys = { keys::solvent_molecular_weight, keys::solvent_density }; + if (object[keys::henry_law_constant]) + nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); + } + else if (type == keys::DissolvedEquilibrium_key) + { + required_keys = { keys::type, keys::condensed_phase, keys::solvent, + keys::reactants, keys::products, keys::algebraic_species, + keys::equilibrium_constant }; + if (object[keys::reactants]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::reactants]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::products]) + { + auto e = CheckReactantsOrProductsSchema(object[keys::products]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + if (object[keys::equilibrium_constant]) + { + auto e = CheckArrheniusReferenceTemperatureSchema(object[keys::equilibrium_constant]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + } + else if (type == keys::LinearConstraint_key) + { + required_keys = { keys::type, keys::algebraic_phase, keys::algebraic_species, keys::terms }; + optional_keys = { keys::name, keys::constant, keys::diagnose_from_state }; + if (object[keys::terms]) + { + auto e = CheckLinearConstraintTermsSchema(object[keys::terms]); + nested_errors.insert(nested_errors.end(), e.begin(), e.end()); + } + // The RHS constant is given either as a fixed value or diagnosed from state, not both. + if (object[keys::constant] && object[keys::diagnose_from_state]) + { + const auto& node = object[keys::diagnose_from_state]; + ErrorLocation error_location{ node.Mark().line, node.Mark().column }; + nested_errors.push_back( + { ErrorCode::MutuallyExclusiveOption, + mc_fmt::format( + "{} error: Mutually exclusive option of '{}' and '{}' found in '{}'.", + error_location, + keys::constant, + keys::diagnose_from_state, + type) }); + } + } + else + { + const auto& node = object[keys::type]; + ErrorLocation error_location{ node.Mark().line, node.Mark().column }; + errors.push_back( + { ErrorCode::UnknownType, + mc_fmt::format("{} error: Unknown aerosol process type '{}' found.", error_location, type) }); + continue; + } + + auto schema_errors = CheckSchema(object, required_keys, optional_keys); + errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); + errors.insert(errors.end(), nested_errors.begin(), nested_errors.end()); + } + + return errors; + } + + } // namespace v1 +} // namespace mechanism_configuration From 0fb6d3db6626c0012d011bac7a56af63e5da44ba Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 14:41:04 -0600 Subject: [PATCH 08/23] replace species with name to reuse the general parser --- include/mechanism_configuration/mechanism.hpp | 3 + src/detail/v1/aerosol_keys.hpp | 2 + src/v1/parser.cpp | 94 +++++++++++++++++-- .../cam_clound_chemistry.json | 56 +++++------ 4 files changed, 118 insertions(+), 37 deletions(-) diff --git a/include/mechanism_configuration/mechanism.hpp b/include/mechanism_configuration/mechanism.hpp index d6201ad2..5c345167 100644 --- a/include/mechanism_configuration/mechanism.hpp +++ b/include/mechanism_configuration/mechanism.hpp @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -85,6 +86,8 @@ namespace mechanism_configuration /// @brief Represents a collection of different reaction types, each stored in a vector /// corresponding to a specific mechanism types::Reactions reactions; + /// @brief Aerosol representations, processes, and constraints + types::Aerosol aerosol; /// @brief Version of the mechanism configuration format used, in major.minor.patch format Version version; /// @brief Relative tolerance for solver (optional, default: 1e-6) diff --git a/src/detail/v1/aerosol_keys.hpp b/src/detail/v1/aerosol_keys.hpp index c93825e3..abf0e054 100644 --- a/src/detail/v1/aerosol_keys.hpp +++ b/src/detail/v1/aerosol_keys.hpp @@ -39,6 +39,8 @@ namespace mechanism_configuration::v1::keys inline constexpr std::string_view equilibrium_constant = "equilibrium constant"; inline constexpr std::string_view reference_temperature = "T0 [K]"; + inline constexpr std::string_view ArrheniusReferenceTemperature_key = "ARRHENIUS_REFERENCE_TEMPERATURE"; + inline constexpr std::string_view henry_law_constant = "Henry's law constant"; inline constexpr std::string_view HLC_ref = "HLC_ref [mol m-3 Pa-1]"; inline constexpr std::string_view henry_law_C = "C [K]"; diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index d5b01c1a..0967f9dd 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -5,6 +5,9 @@ #include "detail/v1/parser.hpp" #include "detail/check_schema.hpp" +#include "detail/v1/aerosol_keys.hpp" +#include "detail/v1/aerosol_type_parsers.hpp" +#include "detail/v1/aerosol_type_schema.hpp" #include "detail/v1/keys.hpp" #include "detail/v1/type_parsers.hpp" #include "detail/v1/type_schema.hpp" @@ -211,8 +214,14 @@ namespace mechanism_configuration::v1 { Errors errors; - std::vector required_keys = { keys::version, keys::species, keys::phases, keys::reactions }; - std::vector optional_keys = { keys::name }; + // A single configuration can contain both gas-phase reactions and aerosol processes. + // Since aerosol chemistry does not always require gas-phase reactions, either section + // may be omitted, making both configurations optional. + std::vector required_keys = { keys::version, keys::species, keys::phases }; + std::vector optional_keys = { keys::name, + keys::reactions, + keys::aerosol_representations, + keys::aerosol_processes }; // Return early if the required keys are not found auto schema_errors = mechanism_configuration::CheckSchema(object, required_keys, optional_keys); @@ -223,6 +232,36 @@ namespace mechanism_configuration::v1 return errors; } + // A config must define at least one mechanism mode: gas-phase reactions, or the aerosol + // pair 'aerosol representations' + 'aerosol processes'. The two modes may also coexist. + const bool has_reactions = static_cast(object[keys::reactions]); + const bool has_aerosol_representations = static_cast(object[keys::aerosol_representations]); + const bool has_aerosol_processes = static_cast(object[keys::aerosol_processes]); + if (has_aerosol_representations != has_aerosol_processes) + { + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + errors.push_back({ ErrorCode::RequiredKeyNotFound, + mc_fmt::format("{} error: '{}' and '{}' must be provided together.", + error_location, + keys::aerosol_representations, + keys::aerosol_processes) }); + } + if (!has_reactions && !(has_aerosol_representations && has_aerosol_processes)) + { + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + errors.push_back({ ErrorCode::RequiredKeyNotFound, + mc_fmt::format("{} error: A configuration must contain either '{}' or both '{}' and '{}'.", + error_location, + keys::reactions, + keys::aerosol_representations, + keys::aerosol_processes) }); + } + if (!errors.empty()) + { + AppendFilePath(config_path_, errors); + return errors; + } + constexpr unsigned int MAJOR_VERSION = 1; Version version = Version(object[keys::version].as()); if (version.major != MAJOR_VERSION) @@ -237,6 +276,8 @@ namespace mechanism_configuration::v1 errors.push_back({ ErrorCode::InvalidVersion, config_path_ + ":" + message }); } + // Species and phases are foundational. If either is invalid, fail fast since all downstream + // validation depends on them. schema_errors = CheckSpeciesSchema(object[keys::species]); if (!schema_errors.empty()) { @@ -257,12 +298,36 @@ namespace mechanism_configuration::v1 auto parsed_phases = ParsePhases(object[keys::phases]); - schema_errors = CheckReactionsSchema(object[keys::reactions], parsed_species, parsed_phases); - if (!schema_errors.empty()) + // Reactions and aerosol sections are independent, so we accumulate errors from both and report + // them together. + + // Gas-phase reactions are optional (an aerosol-only config may omit them). + if (has_reactions) { - AppendFilePath(config_path_, schema_errors); - errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); - return errors; + schema_errors = CheckReactionsSchema(object[keys::reactions], parsed_species, parsed_phases); + if (!schema_errors.empty()) + { + AppendFilePath(config_path_, schema_errors); + errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); + } + } + + // Aerosol sections are optional. + if (has_aerosol_representations && has_aerosol_processes) + { + schema_errors = CheckAerosolRepresentationsSchema(object[keys::aerosol_representations]); + if (!schema_errors.empty()) + { + AppendFilePath(config_path_, schema_errors); + errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); + } + + schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes]); + if (!schema_errors.empty()) + { + AppendFilePath(config_path_, schema_errors); + errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); + } } return errors; @@ -340,8 +405,19 @@ namespace mechanism_configuration::v1 mechanism.version = Version(object[keys::version].as()); mechanism.species = ParseSpecies(object[keys::species]); mechanism.phases = ParsePhases(object[keys::phases]); - mechanism.reactions = ParseReactions(object[keys::reactions]); - + + if (object[keys::reactions]) + { + mechanism.reactions = ParseReactions(object[keys::reactions]); + } + if (object[keys::aerosol_representations]) + { + mechanism.aerosol.representations = ParseAerosolRepresentations(object[keys::aerosol_representations]); + } + if (object[keys::aerosol_processes]) + { + ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.aerosol); + } if (object[keys::name]) { mechanism.name = object[keys::name].as(); diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json index 829221b4..b45f6103 100644 --- a/test/integration/integration_configs/cam_clound_chemistry.json +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -95,12 +95,12 @@ "condensed phase": "AQUEOUS", "solvent": "H2O", "reactants": [ - { "species": "HSO3m", "coefficient": 1 }, - { "species": "H2O2", "coefficient": 1 } + { "name": "HSO3m", "coefficient": 1 }, + { "name": "H2O2", "coefficient": 1 } ], "products": [ - { "species": "SO2OOHm", "coefficient": 1 }, - { "species": "H2O", "coefficient": 1 } + { "name": "SO2OOHm", "coefficient": 1 }, + { "name": "H2O", "coefficient": 1 } ], "forward rate constants": { "CLOUD": { "type": "ARRHENIUS", "A": 3.184e8, "C": 4430.0 } @@ -132,11 +132,11 @@ "condensed phase": "AQUEOUS", "solvent": "H2O", "reactants": [ - { "species": "H2O", "coefficient": 2 } + { "name": "H2O", "coefficient": 2 } ], "products": [ - { "species": "Hp", "coefficient": 1 }, - { "species": "OHm", "coefficient": 1 } + { "name": "Hp", "coefficient": 1 }, + { "name": "OHm", "coefficient": 1 } ], "algebraic species": "OHm", "equilibrium constant": { @@ -149,11 +149,11 @@ "condensed phase": "AQUEOUS", "solvent": "H2O", "reactants": [ - { "species": "SO2", "coefficient": 1 } + { "name": "SO2", "coefficient": 1 } ], "products": [ - { "species": "Hp", "coefficient": 1 }, - { "species": "HSO3m", "coefficient": 1 } + { "name": "Hp", "coefficient": 1 }, + { "name": "HSO3m", "coefficient": 1 } ], "algebraic species": "HSO3m", "equilibrium constant": { @@ -167,11 +167,11 @@ "algebraic species": "SO3mm", "solvent": "H2O", "reactants": [ - { "species": "HSO3m", "coefficient": 1 } + { "name": "HSO3m", "coefficient": 1 } ], "products": [ - { "species": "Hp", "coefficient": 1 }, - { "species": "SO3mm", "coefficient": 1 } + { "name": "Hp", "coefficient": 1 }, + { "name": "SO3mm", "coefficient": 1 } ], "equilibrium constant": { "type": "ARRHENIUS_REFERENCE_TEMPERATURE", "A": 1.08e-9, "C [K]": 1120.0, "T0 [K]": 298.15 @@ -184,11 +184,11 @@ "algebraic species": "SO2", "diagnose from state": true, "terms": [ - { "phase": "gas", "species": "SO2", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "SO2", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "HSO3m", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "SO3mm", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "SO2OOHm", "coefficient": 1.0 } + { "phase": "gas", "name": "SO2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "SO2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "HSO3m", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "SO3mm", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "SO2OOHm", "coefficient": 1.0 } ] }, { @@ -198,8 +198,8 @@ "algebraic species": "H2O2", "diagnose from state": true, "terms": [ - { "phase": "gas", "species": "H2O2", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "H2O2", "coefficient": 1.0 } + { "phase": "gas", "name": "H2O2", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "H2O2", "coefficient": 1.0 } ] }, { @@ -209,8 +209,8 @@ "algebraic species": "O3", "diagnose from state": true, "terms": [ - { "phase": "gas", "species": "O3", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "O3", "coefficient": 1.0 } + { "phase": "gas", "name": "O3", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "O3", "coefficient": 1.0 } ] }, { @@ -220,12 +220,12 @@ "algebraic species": "Hp", "constant [mol m-3]": 0.0, "terms": [ - { "phase": "AQUEOUS", "species": "Hp", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "species": "OHm", "coefficient": -1.0 }, - { "phase": "AQUEOUS", "species": "HSO3m", "coefficient": -1.0 }, - { "phase": "AQUEOUS", "species": "SO3mm", "coefficient": -2.0 }, - { "phase": "AQUEOUS", "species": "SO4mm", "coefficient": -2.0 }, - { "phase": "AQUEOUS", "species": "SO2OOHm", "coefficient": -1.0 } + { "phase": "AQUEOUS", "name": "Hp", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "OHm", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "name": "HSO3m", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "name": "SO3mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "name": "SO4mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "name": "SO2OOHm", "coefficient": -1.0 } ] } ] From 01629dfea43f2e53902a12162cd53aa4dbd0d50f Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 18:18:39 -0600 Subject: [PATCH 09/23] remove duplicate functions --- .../mechanism_configuration/aerosol_types.hpp | 5 +-- src/detail/v1/aerosol_keys.hpp | 2 +- src/detail/v1/aerosol_type_parsers.hpp | 7 ++++ src/v1/aerosol_type_parsers.cpp | 38 ++++++++++++++++--- src/v1/aerosol_type_schema.cpp | 22 +++++++++-- .../cam_clound_chemistry.json | 18 ++++----- 6 files changed, 69 insertions(+), 23 deletions(-) diff --git a/include/mechanism_configuration/aerosol_types.hpp b/include/mechanism_configuration/aerosol_types.hpp index 13017b78..0574163f 100644 --- a/include/mechanism_configuration/aerosol_types.hpp +++ b/include/mechanism_configuration/aerosol_types.hpp @@ -39,8 +39,7 @@ namespace mechanism_configuration::types }; /// @brief A reaction rate constant parsed from config. - // using RateConstant = std::variant>; // TODO - using RateConstant = std::variant>; + using RateConstant = std::variant>; // ---------------------------------------- // Representations @@ -142,7 +141,7 @@ namespace mechanism_configuration::types struct LinearConstraintTerm { std::string phase; - std::string species; + std::string name; double coefficient; }; diff --git a/src/detail/v1/aerosol_keys.hpp b/src/detail/v1/aerosol_keys.hpp index abf0e054..f068be26 100644 --- a/src/detail/v1/aerosol_keys.hpp +++ b/src/detail/v1/aerosol_keys.hpp @@ -81,7 +81,7 @@ namespace mechanism_configuration::v1::keys inline constexpr std::string_view algebraic_species = "algebraic species"; // LinearConstraint - // also: name; terms carry coefficient and species + // also: name; terms carry coefficient and name inline constexpr std::string_view LinearConstraint_key = "LINEAR_CONSTRAINT"; inline constexpr std::string_view algebraic_phase = "algebraic phase"; inline constexpr std::string_view terms = "terms"; diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp index 033cc585..7c0f99bb 100644 --- a/src/detail/v1/aerosol_type_parsers.hpp +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -23,6 +23,13 @@ namespace mechanism_configuration::v1 types::HenryLawConstant ParseHenryLawConstant(const YAML::Node& object); types::ArrheniusReferenceTemperature ParseArrheniusReferenceTemperature(const YAML::Node& object); + /// @brief Parses a bare Arrhenius rate-constant block (A, B, C, D, E). + types::Arrhenius ParseArrhenius(const YAML::Node& object); + + /// @brief Parses one rate-constant block into the RateConstant variant, dispatching on its + /// inner `type` (ARRHENIUS -> Arrhenius, ARRHENIUS_REFERENCE_TEMPERATURE -> Arrhenius reference temperature). + types::RateConstant ParseRateConstant(const YAML::Node& object); + /// @brief Parses a per-representation rate-constant map, e.g. { "CLOUD": { ... } }. /// @return Map keyed by representation name to its rate constant. std::map ParseRateConstantMap(const YAML::Node& object); diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index d0eb48b5..b69c7c66 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -21,14 +21,40 @@ namespace mechanism_configuration types::ArrheniusReferenceTemperature rate_constant; rate_constant.A = object[keys::A].as(); - if (object[keys::C]) - rate_constant.C = object[keys::C].as(); + if (object[keys::henry_law_C]) + rate_constant.C = object[keys::henry_law_C].as(); if (object[keys::reference_temperature]) rate_constant.T0 = object[keys::reference_temperature].as(); return rate_constant; } + types::Arrhenius ParseArrhenius(const YAML::Node& object) + { + types::Arrhenius rate_constant; + + rate_constant.A = object[keys::A].as(); + if (object[keys::B]) + rate_constant.B = object[keys::B].as(); + if (object[keys::C]) + rate_constant.C = object[keys::C].as(); + if (object[keys::D]) + rate_constant.D = object[keys::D].as(); + if (object[keys::E]) + rate_constant.E = object[keys::E].as(); + + return rate_constant; + } + + types::RateConstant ParseRateConstant(const YAML::Node& object) + { + if (object[keys::type] && object[keys::type].as() == keys::ArrheniusReferenceTemperature_key) + return ParseArrheniusReferenceTemperature(object); + + // ARRHENIUS is the default when no (recognized) type is given. + return ParseArrhenius(object); + } + types::HenryLawConstant ParseHenryLawConstant(const YAML::Node& object) { types::HenryLawConstant henry_law_constant; @@ -46,11 +72,11 @@ namespace mechanism_configuration { std::map rate_constants; - // Each key is an aerosol representation name (e.g. "CLOUD"); each value is a - // reference-temperature Arrhenius block. + // Each key is an aerosol representation name (e.g. "CLOUD"); each value is a rate-constant + // block whose own `type` selects the RateConstant variant alternative. for (const auto& entry : object) { - rate_constants.emplace(entry.first.as(), ParseArrheniusReferenceTemperature(entry.second)); + rate_constants.emplace(entry.first.as(), ParseRateConstant(entry.second)); } return rate_constants; @@ -209,7 +235,7 @@ namespace mechanism_configuration { types::LinearConstraintTerm term; term.phase = term_node[keys::phase].as(); - term.species = term_node[keys::species].as(); + term.name = term_node[keys::name].as(); term.coefficient = term_node[keys::coefficient].as(); constraint.terms.push_back(term); } diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp index 18c59534..41f1c1f0 100644 --- a/src/v1/aerosol_type_schema.cpp +++ b/src/v1/aerosol_type_schema.cpp @@ -24,11 +24,25 @@ namespace mechanism_configuration { Errors CheckArrheniusReferenceTemperatureSchema(const YAML::Node& object) { - const std::vector required_keys = { keys::A, keys::C }; + const std::vector required_keys = { keys::A, keys::henry_law_C }; const std::vector optional_keys = { keys::reference_temperature }; return CheckSchema(object, required_keys, optional_keys); } + Errors CheckArrheniusSchema(const YAML::Node& object) + { + const std::vector required_keys = { keys::A, keys::C }; + const std::vector optional_keys = { keys::B, keys::D, keys::E }; + return CheckSchema(object, required_keys, optional_keys); + } + + Errors CheckRateConstantSchema(const YAML::Node& object) + { + if (object[keys::type] && object[keys::type].as() == keys::ArrheniusReferenceTemperature_key) + return CheckArrheniusReferenceTemperatureSchema(object); + return CheckArrheniusSchema(object); + } + Errors CheckHenryLawConstantSchema(const YAML::Node& object) { const std::vector required_keys = { keys::HLC_ref, keys::henry_law_C }; @@ -36,13 +50,13 @@ namespace mechanism_configuration return CheckSchema(object, required_keys, optional_keys); } - // A per-representation rate-constant map; each value is a reference-temperature block. + // A per-representation rate-constant map. Each value is a typed rate-constant block. Errors CheckRateConstantMapSchema(const YAML::Node& object) { Errors errors; for (const auto& entry : object) { - auto entry_errors = CheckArrheniusReferenceTemperatureSchema(entry.second); // TODO + auto entry_errors = CheckRateConstantSchema(entry.second); errors.insert(errors.end(), entry_errors.begin(), entry_errors.end()); } return errors; @@ -52,7 +66,7 @@ namespace mechanism_configuration Errors CheckLinearConstraintTermsSchema(const YAML::Node& object) { Errors errors; - const std::vector required_keys = { keys::phase, keys::species, keys::coefficient }; + const std::vector required_keys = { keys::phase, keys::name, keys::coefficient }; const std::vector optional_keys = {}; for (const auto& term : object) { diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/test/integration/integration_configs/cam_clound_chemistry.json index b45f6103..0755db00 100644 --- a/test/integration/integration_configs/cam_clound_chemistry.json +++ b/test/integration/integration_configs/cam_clound_chemistry.json @@ -116,11 +116,11 @@ "condensed phase": "AQUEOUS", "solvent": "H2O", "reactants": [ - { "species name": "SO2OOHm", "coefficient": 1 }, - { "species name": "Hp", "coefficient": 1 } + { "name": "SO2OOHm", "coefficient": 1 }, + { "name": "Hp", "coefficient": 1 } ], "products": [ - { "species name": "SO4mm", "coefficient": 1 } + { "name": "SO4mm", "coefficient": 1 } ], "rate constants": { "CLOUD": { "type": "ARRHENIUS", "A": 1.333e8, "C": 4430.0} @@ -220,12 +220,12 @@ "algebraic species": "Hp", "constant [mol m-3]": 0.0, "terms": [ - { "phase": "AQUEOUS", "name": "Hp", "coefficient": 1.0 }, - { "phase": "AQUEOUS", "name": "OHm", "coefficient": -1.0 }, - { "phase": "AQUEOUS", "name": "HSO3m", "coefficient": -1.0 }, - { "phase": "AQUEOUS", "name": "SO3mm", "coefficient": -2.0 }, - { "phase": "AQUEOUS", "name": "SO4mm", "coefficient": -2.0 }, - { "phase": "AQUEOUS", "name": "SO2OOHm", "coefficient": -1.0 } + { "phase": "AQUEOUS", "name": "Hp", "coefficient": 1.0 }, + { "phase": "AQUEOUS", "name": "OHm", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "name": "HSO3m", "coefficient": -1.0 }, + { "phase": "AQUEOUS", "name": "SO3mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "name": "SO4mm", "coefficient": -2.0 }, + { "phase": "AQUEOUS", "name": "SO2OOHm", "coefficient": -1.0 } ] } ] From b3b4b0172823f41fe0d3c937cf0e6050bbfef504 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 18:49:36 -0600 Subject: [PATCH 10/23] gas diffusion coefficient --- src/detail/v1/aerosol_keys.hpp | 2 +- src/detail/v1/aerosol_type_parsers.hpp | 13 +++++++++++-- src/v1/aerosol_type_schema.cpp | 8 ++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/detail/v1/aerosol_keys.hpp b/src/detail/v1/aerosol_keys.hpp index f068be26..840e0cdf 100644 --- a/src/detail/v1/aerosol_keys.hpp +++ b/src/detail/v1/aerosol_keys.hpp @@ -54,7 +54,7 @@ namespace mechanism_configuration::v1::keys // also: gas_phase, gas_phase_species, condensed_phase, condensed_phase_species, solvent, // henry_law_constant, diffusion_coefficient inline constexpr std::string_view HenryLawPhaseTransfer_key = "HENRY_LAW_PHASE_TRANSFER"; - inline constexpr std::string_view accommodation_coefficient = "accommodation coefficient [-]"; + inline constexpr std::string_view accommodation_coefficient = "accommodation coefficient"; // DissolvedReaction // also: condensed_phase, solvent, reactants, products, rate_constants diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp index 7c0f99bb..a75a93ce 100644 --- a/src/detail/v1/aerosol_type_parsers.hpp +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -46,7 +46,11 @@ namespace mechanism_configuration::v1 // Process parsers // ---------------------------------------- - types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer(const YAML::Node& object); + /// @brief Parses a Henry's-law phase transfer. The diffusion coefficient is sourced from the + /// gas-phase species' definition in `phases`. + types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer( + const YAML::Node& object, + const std::vector& phases); types::DissolvedReaction ParseDissolvedReaction(const YAML::Node& object); types::DissolvedReversibleReaction ParseDissolvedReversibleReaction(const YAML::Node& object); @@ -65,6 +69,11 @@ namespace mechanism_configuration::v1 std::vector ParseAerosolRepresentations(const YAML::Node& objects); /// @brief Parses the aerosol processes sequence into the processes or constraints - void ParseAerosolProcesses(const YAML::Node& objects, types::Aerosol& aerosol); + /// @param phases Parsed phases, used to source per-species values such as a phase-transfer's + /// gas-phase diffusion coefficient + void ParseAerosolProcesses( + const YAML::Node& objects, + const std::vector& phases, + types::Aerosol& aerosol); } // namespace mechanism_configuration::v1 diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp index 41f1c1f0..d2b849ca 100644 --- a/src/v1/aerosol_type_schema.cpp +++ b/src/v1/aerosol_type_schema.cpp @@ -25,14 +25,14 @@ namespace mechanism_configuration Errors CheckArrheniusReferenceTemperatureSchema(const YAML::Node& object) { const std::vector required_keys = { keys::A, keys::henry_law_C }; - const std::vector optional_keys = { keys::reference_temperature }; + const std::vector optional_keys = { keys::type, keys::reference_temperature }; return CheckSchema(object, required_keys, optional_keys); } Errors CheckArrheniusSchema(const YAML::Node& object) { const std::vector required_keys = { keys::A, keys::C }; - const std::vector optional_keys = { keys::B, keys::D, keys::E }; + const std::vector optional_keys = { keys::type }; return CheckSchema(object, required_keys, optional_keys); } @@ -223,7 +223,7 @@ namespace mechanism_configuration keys::condensed_phase_species, keys::solvent, keys::henry_law_constant }; - optional_keys = { keys::solvent_molecular_weight, keys::solvent_density }; + optional_keys = { keys::solvent_molecular_weight, keys::solvent_density }; // TODO if (object[keys::henry_law_constant]) nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); } @@ -251,7 +251,7 @@ namespace mechanism_configuration else if (type == keys::LinearConstraint_key) { required_keys = { keys::type, keys::algebraic_phase, keys::algebraic_species, keys::terms }; - optional_keys = { keys::name, keys::constant, keys::diagnose_from_state }; + optional_keys = { keys::constant, keys::diagnose_from_state }; if (object[keys::terms]) { auto e = CheckLinearConstraintTermsSchema(object[keys::terms]); From cd913cc53fe40d0bb1001eb5f2882556289e7cd0 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 19:05:29 -0600 Subject: [PATCH 11/23] support reading diffusion coefficieint form phases species --- src/detail/v1/aerosol_type_schema.hpp | 7 ++++++- src/detail/v1/utils.hpp | 12 ++++++++++++ src/v1/aerosol_type_parsers.cpp | 16 ++++++++++++---- src/v1/aerosol_type_schema.cpp | 27 ++++++++++++++++++++++++--- src/v1/parser.cpp | 4 ++-- src/v1/utils.cpp | 18 ++++++++++++++++++ 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/detail/v1/aerosol_type_schema.hpp b/src/detail/v1/aerosol_type_schema.hpp index 331ef536..3bc23ca1 100644 --- a/src/detail/v1/aerosol_type_schema.hpp +++ b/src/detail/v1/aerosol_type_schema.hpp @@ -5,9 +5,12 @@ #pragma once #include +#include #include +#include + namespace mechanism_configuration::v1 { /// @brief Schema-validates each entry of the aerosol representations section. @@ -18,7 +21,9 @@ namespace mechanism_configuration::v1 /// @brief Schema-validates each entry of the aerosol processes section which mixes /// process and constraint types. /// @param processes_list YAML node containing the process/constraint entries + /// @param phases Parsed phases, used to validate cross-references such as a phase-transfer's + /// gas-phase species carrying a diffusion coefficient /// @return List of structural errors, or empty if all entries conform - Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list); + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list, const std::vector& phases); } // namespace mechanism_configuration::v1 diff --git a/src/detail/v1/utils.hpp b/src/detail/v1/utils.hpp index 4411c8a3..8134b2f1 100644 --- a/src/detail/v1/utils.hpp +++ b/src/detail/v1/utils.hpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -33,4 +34,15 @@ namespace mechanism_configuration::v1 /// @note Assumes the component has already been validated to contain exactly one of them. std::string GetReactionComponentName(const YAML::Node& component); + /// @brief Looks up the diffusion coefficient defined for a species within a phase. + /// @param phases Parsed phases to search + /// @param phase_name Name of the phase that should contain the species + /// @param species_name Name of the species whose diffusion coefficient is requested + /// @return The diffusion coefficient, or nullopt if the phase/species is not found or the + /// species has no diffusion coefficient defined. + std::optional FindPhaseSpeciesDiffusionCoefficient( + const std::vector& phases, + const std::string& phase_name, + const std::string& species_name); + } // namespace mechanism_configuration::v1 diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index b69c7c66..7a3f211b 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -7,6 +7,7 @@ #include "detail/v1/aerosol_keys.hpp" #include "detail/v1/keys.hpp" #include "detail/v1/type_parsers.hpp" +#include "detail/v1/utils.hpp" namespace mechanism_configuration { @@ -136,7 +137,9 @@ namespace mechanism_configuration // Process parsers // ---------------------------------------- - types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer(const YAML::Node& object) + types::HenryLawPhaseTransfer ParseHenryLawPhaseTransfer( + const YAML::Node& object, + const std::vector& phases) { types::HenryLawPhaseTransfer transfer; @@ -146,7 +149,9 @@ namespace mechanism_configuration transfer.condensed_species = object[keys::condensed_phase_species].as(); transfer.solvent = object[keys::solvent].as(); transfer.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); - transfer.diffusion_coefficient = object[keys::diffusion_coefficient].as(); + // Sourced from the gas-phase species' definition. Schema validation guarantees it is present. + transfer.diffusion_coefficient = + FindPhaseSpeciesDiffusionCoefficient(phases, transfer.gas_phase, transfer.gas_species).value(); transfer.accommodation_coefficient = object[keys::accommodation_coefficient].as(); return transfer; @@ -272,7 +277,10 @@ namespace mechanism_configuration return representations; } - void ParseAerosolProcesses(const YAML::Node& objects, types::Aerosol& aerosol) + void ParseAerosolProcesses( + const YAML::Node& objects, + const std::vector& phases, + types::Aerosol& aerosol) { for (const auto& object : objects) { @@ -280,7 +288,7 @@ namespace mechanism_configuration // Processes if (type == keys::HenryLawPhaseTransfer_key) - aerosol.processes.emplace_back(ParseHenryLawPhaseTransfer(object)); + aerosol.processes.emplace_back(ParseHenryLawPhaseTransfer(object, phases)); else if (type == keys::DissolvedReaction_key) aerosol.processes.emplace_back(ParseDissolvedReaction(object)); else if (type == keys::DissolvedReversibleReaction_key) diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp index d2b849ca..48472d9d 100644 --- a/src/v1/aerosol_type_schema.cpp +++ b/src/v1/aerosol_type_schema.cpp @@ -129,7 +129,7 @@ namespace mechanism_configuration return errors; } - Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list) + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list, const std::vector& phases) { Errors errors; @@ -152,6 +152,8 @@ namespace mechanism_configuration const std::string type = object[keys::type].as(); if (type == keys::HenryLawPhaseTransfer_key) { + // The diffusion coefficient is not given here; it is sourced from the gas-phase + // species' definition in the phases section. required_keys = { keys::type, keys::gas_phase, keys::gas_phase_species, @@ -159,10 +161,29 @@ namespace mechanism_configuration keys::condensed_phase_species, keys::solvent, keys::henry_law_constant, - keys::diffusion_coefficient, - keys::accommodation_coefficient }; // TODO + keys::accommodation_coefficient }; if (object[keys::henry_law_constant]) nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); + // The gas-phase species must carry a diffusion coefficient, since this process reads it + // from the phases section rather than from the process block itself. + if (object[keys::gas_phase] && object[keys::gas_phase_species]) + { + const auto gas_phase = object[keys::gas_phase].as(); + const auto gas_species = object[keys::gas_phase_species].as(); + if (!FindPhaseSpeciesDiffusionCoefficient(phases, gas_phase, gas_species).has_value()) + { + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + nested_errors.push_back( + { ErrorCode::RequiredKeyNotFound, + mc_fmt::format( + "{} error: '{}' references gas-phase species '{}' in phase '{}', which has no " + "diffusion coefficient defined in the phases section.", + error_location, + keys::HenryLawPhaseTransfer_key, + gas_species, + gas_phase) }); + } + } } else if (type == keys::DissolvedReaction_key) { diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index 0967f9dd..34ac8b7d 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -322,7 +322,7 @@ namespace mechanism_configuration::v1 errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); } - schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes]); + schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes], parsed_phases); if (!schema_errors.empty()) { AppendFilePath(config_path_, schema_errors); @@ -416,7 +416,7 @@ namespace mechanism_configuration::v1 } if (object[keys::aerosol_processes]) { - ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.aerosol); + ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.phases, mechanism.aerosol); } if (object[keys::name]) { diff --git a/src/v1/utils.cpp b/src/v1/utils.cpp index 09e9e2dc..b6727fc5 100644 --- a/src/v1/utils.cpp +++ b/src/v1/utils.cpp @@ -78,5 +78,23 @@ namespace mechanism_configuration return unknown_properties; } + std::optional FindPhaseSpeciesDiffusionCoefficient( + const std::vector& phases, + const std::string& phase_name, + const std::string& species_name) + { + for (const auto& phase : phases) + { + if (phase.name != phase_name) + continue; + for (const auto& species : phase.species) + { + if (species.name == species_name) + return species.diffusion_coefficient; + } + } + return std::nullopt; + } + } // namespace v1 } // namespace mechanism_configuration From f4033f5fd1d8179204c7a09a00bd13832dab4cf0 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 19:12:51 -0600 Subject: [PATCH 12/23] support reading solvent molecular weight and density --- include/mechanism_configuration/types.hpp | 1 + src/detail/v1/aerosol_type_parsers.hpp | 8 ++++- src/detail/v1/aerosol_type_schema.hpp | 9 ++++-- src/detail/v1/utils.hpp | 14 +++++++++ src/v1/aerosol_type_parsers.cpp | 18 ++++++----- src/v1/aerosol_type_schema.cpp | 37 +++++++++++++++++++++-- src/v1/parser.cpp | 4 +-- src/v1/type_parsers.cpp | 4 +++ src/v1/type_schema.cpp | 2 +- src/v1/utils.cpp | 30 ++++++++++++++++++ 10 files changed, 112 insertions(+), 15 deletions(-) diff --git a/include/mechanism_configuration/types.hpp b/include/mechanism_configuration/types.hpp index 6a834f92..2b0c86e5 100644 --- a/include/mechanism_configuration/types.hpp +++ b/include/mechanism_configuration/types.hpp @@ -37,6 +37,7 @@ namespace mechanism_configuration::types { std::string name; std::optional diffusion_coefficient; + std::optional density; /// @brief Unknown properties, prefixed with two underscores (__) std::unordered_map unknown_properties; }; diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp index a75a93ce..0d455be0 100644 --- a/src/detail/v1/aerosol_type_parsers.hpp +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -58,7 +58,12 @@ namespace mechanism_configuration::v1 // Constraint parsers // ---------------------------------------- - types::HenryLawEquilibrium ParseHenryLawEquilibrium(const YAML::Node& object); + /// @brief Parses a Henry's-law equilibrium. The solvent's molecular weight is sourced from the + /// species section and its density from the condensed phase. + types::HenryLawEquilibrium ParseHenryLawEquilibrium( + const YAML::Node& object, + const std::vector& species, + const std::vector& phases); types::DissolvedEquilibrium ParseDissolvedEquilibrium(const YAML::Node& object); types::LinearConstraint ParseLinearConstraint(const YAML::Node& object); @@ -73,6 +78,7 @@ namespace mechanism_configuration::v1 /// gas-phase diffusion coefficient void ParseAerosolProcesses( const YAML::Node& objects, + const std::vector& species, const std::vector& phases, types::Aerosol& aerosol); diff --git a/src/detail/v1/aerosol_type_schema.hpp b/src/detail/v1/aerosol_type_schema.hpp index 3bc23ca1..5f517405 100644 --- a/src/detail/v1/aerosol_type_schema.hpp +++ b/src/detail/v1/aerosol_type_schema.hpp @@ -21,9 +21,14 @@ namespace mechanism_configuration::v1 /// @brief Schema-validates each entry of the aerosol processes section which mixes /// process and constraint types. /// @param processes_list YAML node containing the process/constraint entries + /// @param species Parsed top-level species, used to validate cross-references such as a Henry's-law + /// equilibrium's solvent species carrying a molecular weight /// @param phases Parsed phases, used to validate cross-references such as a phase-transfer's - /// gas-phase species carrying a diffusion coefficient + /// gas-phase species carrying a diffusion coefficient, or a solvent's density /// @return List of structural errors, or empty if all entries conform - Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list, const std::vector& phases); + Errors CheckAerosolProcessesSchema( + const YAML::Node& processes_list, + const std::vector& species, + const std::vector& phases); } // namespace mechanism_configuration::v1 diff --git a/src/detail/v1/utils.hpp b/src/detail/v1/utils.hpp index 8134b2f1..ddbb2dbe 100644 --- a/src/detail/v1/utils.hpp +++ b/src/detail/v1/utils.hpp @@ -45,4 +45,18 @@ namespace mechanism_configuration::v1 const std::string& phase_name, const std::string& species_name); + /// @brief Looks up the density defined for a species within a phase. + /// @return The density, or nullopt if the phase/species is not found or the species has no + /// density defined. + std::optional FindPhaseSpeciesDensity( + const std::vector& phases, + const std::string& phase_name, + const std::string& species_name); + + /// @brief Looks up the molecular weight defined for a top-level species. + /// @return The molecular weight, or nullopt if the species is not found or has none defined. + std::optional FindSpeciesMolecularWeight( + const std::vector& species, + const std::string& species_name); + } // namespace mechanism_configuration::v1 diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index 7a3f211b..24ea6b96 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -195,7 +195,10 @@ namespace mechanism_configuration // Constraint parsers // ---------------------------------------- - types::HenryLawEquilibrium ParseHenryLawEquilibrium(const YAML::Node& object) + types::HenryLawEquilibrium ParseHenryLawEquilibrium( + const YAML::Node& object, + const std::vector& species, + const std::vector& phases) { types::HenryLawEquilibrium equilibrium; @@ -206,11 +209,11 @@ namespace mechanism_configuration equilibrium.solvent = object[keys::solvent].as(); equilibrium.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); - // Solvent properties are optional here; when omitted they are sourced from the solvent - // species definition downstream. - equilibrium.solvent_molecular_weight = - object[keys::solvent_molecular_weight] ? object[keys::solvent_molecular_weight].as() : 0.0; - equilibrium.solvent_density = object[keys::solvent_density] ? object[keys::solvent_density].as() : 0.0; + // Sourced from the solvent species' definition: molecular weight from the species section, + // density from the condensed phase. Schema validation guarantees both are present. + equilibrium.solvent_molecular_weight = FindSpeciesMolecularWeight(species, equilibrium.solvent).value(); + equilibrium.solvent_density = + FindPhaseSpeciesDensity(phases, equilibrium.condensed_phase, equilibrium.solvent).value(); return equilibrium; } @@ -279,6 +282,7 @@ namespace mechanism_configuration void ParseAerosolProcesses( const YAML::Node& objects, + const std::vector& species, const std::vector& phases, types::Aerosol& aerosol) { @@ -295,7 +299,7 @@ namespace mechanism_configuration aerosol.processes.emplace_back(ParseDissolvedReversibleReaction(object)); // Constraints else if (type == keys::HenryLawEquilibrium_key) - aerosol.constraints.emplace_back(ParseHenryLawEquilibrium(object)); + aerosol.constraints.emplace_back(ParseHenryLawEquilibrium(object, species, phases)); else if (type == keys::DissolvedEquilibrium_key) aerosol.constraints.emplace_back(ParseDissolvedEquilibrium(object)); else if (type == keys::LinearConstraint_key) diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp index 48472d9d..381f4a08 100644 --- a/src/v1/aerosol_type_schema.cpp +++ b/src/v1/aerosol_type_schema.cpp @@ -129,7 +129,10 @@ namespace mechanism_configuration return errors; } - Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list, const std::vector& phases) + Errors CheckAerosolProcessesSchema( + const YAML::Node& processes_list, + const std::vector& species, + const std::vector& phases) { Errors errors; @@ -237,6 +240,9 @@ namespace mechanism_configuration } else if (type == keys::HenryLawEquilibrium_key) { + // The solvent's molecular weight and density are not given here; they are sourced from + // the solvent species' definition (molecular weight from the species section, density + // from the condensed phase). required_keys = { keys::type, keys::gas_phase, keys::gas_phase_species, @@ -244,9 +250,36 @@ namespace mechanism_configuration keys::condensed_phase_species, keys::solvent, keys::henry_law_constant }; - optional_keys = { keys::solvent_molecular_weight, keys::solvent_density }; // TODO if (object[keys::henry_law_constant]) nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); + if (object[keys::solvent] && object[keys::condensed_phase]) + { + const auto solvent = object[keys::solvent].as(); + const auto condensed_phase = object[keys::condensed_phase].as(); + ErrorLocation error_location{ object.Mark().line, object.Mark().column }; + if (!FindSpeciesMolecularWeight(species, solvent).has_value()) + { + nested_errors.push_back( + { ErrorCode::RequiredKeyNotFound, + mc_fmt::format( + "{} error: '{}' solvent species '{}' has no molecular weight defined in the " + "species section.", + error_location, + keys::HenryLawEquilibrium_key, + solvent) }); + } + if (!FindPhaseSpeciesDensity(phases, condensed_phase, solvent).has_value()) + { + nested_errors.push_back( + { ErrorCode::RequiredKeyNotFound, + mc_fmt::format( + "{} error: '{}' solvent species '{}' has no density defined in phase '{}'.", + error_location, + keys::HenryLawEquilibrium_key, + solvent, + condensed_phase) }); + } + } } else if (type == keys::DissolvedEquilibrium_key) { diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index 34ac8b7d..ff41f562 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -322,7 +322,7 @@ namespace mechanism_configuration::v1 errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); } - schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes], parsed_phases); + schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes], parsed_species, parsed_phases); if (!schema_errors.empty()) { AppendFilePath(config_path_, schema_errors); @@ -416,7 +416,7 @@ namespace mechanism_configuration::v1 } if (object[keys::aerosol_processes]) { - ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.phases, mechanism.aerosol); + ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.species, mechanism.phases, mechanism.aerosol); } if (object[keys::name]) { diff --git a/src/v1/type_parsers.cpp b/src/v1/type_parsers.cpp index cdf54a2c..3442f4c6 100644 --- a/src/v1/type_parsers.cpp +++ b/src/v1/type_parsers.cpp @@ -80,6 +80,10 @@ namespace mechanism_configuration { phase_species.diffusion_coefficient = spec[keys::diffusion_coefficient].as(); } + if (spec[keys::density]) + { + phase_species.density = spec[keys::density].as(); + } phase_species.unknown_properties = GetComments(spec); } diff --git a/src/v1/type_schema.cpp b/src/v1/type_schema.cpp index a627c389..fed3dd4d 100644 --- a/src/v1/type_schema.cpp +++ b/src/v1/type_schema.cpp @@ -51,7 +51,7 @@ namespace mechanism_configuration const std::vector optional_keys = {}; // PhaseSpecies const std::vector species_required_keys = { keys::name }; - const std::vector species_optional_keys = { keys::diffusion_coefficient }; + const std::vector species_optional_keys = { keys::diffusion_coefficient, keys::density }; // Structural validation only. Duplicate detection, phase-species existence, and // phase-membership (semantic checks) are performed by the version-neutral diff --git a/src/v1/utils.cpp b/src/v1/utils.cpp index b6727fc5..8da0c9d2 100644 --- a/src/v1/utils.cpp +++ b/src/v1/utils.cpp @@ -96,5 +96,35 @@ namespace mechanism_configuration return std::nullopt; } + std::optional FindPhaseSpeciesDensity( + const std::vector& phases, + const std::string& phase_name, + const std::string& species_name) + { + for (const auto& phase : phases) + { + if (phase.name != phase_name) + continue; + for (const auto& species : phase.species) + { + if (species.name == species_name) + return species.density; + } + } + return std::nullopt; + } + + std::optional FindSpeciesMolecularWeight( + const std::vector& species, + const std::string& species_name) + { + for (const auto& s : species) + { + if (s.name == species_name) + return s.molecular_weight; + } + return std::nullopt; + } + } // namespace v1 } // namespace mechanism_configuration From 4f2709329afe0a4022ee267a6e14a2cdf405e0a6 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 19:46:21 -0600 Subject: [PATCH 13/23] add integration test --- .../v1/cam_cloud_chemistry.json | 0 src/v1/parser.cpp | 2 + test/integration/test_parser.cpp | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+) rename test/integration/integration_configs/cam_clound_chemistry.json => examples/v1/cam_cloud_chemistry.json (100%) diff --git a/test/integration/integration_configs/cam_clound_chemistry.json b/examples/v1/cam_cloud_chemistry.json similarity index 100% rename from test/integration/integration_configs/cam_clound_chemistry.json rename to examples/v1/cam_cloud_chemistry.json diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index ff41f562..e60140a5 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -200,6 +200,8 @@ namespace mechanism_configuration::v1 resolve_section(keys::species); resolve_section(keys::phases); resolve_section(keys::reactions); + resolve_section(keys::aerosol_representations); + resolve_section(keys::aerosol_processes); if (!errors.empty()) { diff --git a/test/integration/test_parser.cpp b/test/integration/test_parser.cpp index a6cb717e..d4d7c95a 100644 --- a/test/integration/test_parser.cpp +++ b/test/integration/test_parser.cpp @@ -71,3 +71,47 @@ TEST(Parse, ParsesV0DirectoryConfiguration) if (parsed) EXPECT_EQ(parsed->version.major, 0); } + +TEST(Parse, ParsesCamCloudChemistryAerosolConfiguration) +{ + auto parsed = Parse("examples/v1/cam_cloud_chemistry.json"); + if (!parsed) + for (const auto& [code, message] : parsed.error()) + std::cout << message << std::endl; + ASSERT_TRUE(parsed); + + const Mechanism& mechanism = *parsed; + EXPECT_EQ(mechanism.version.major, 1); + EXPECT_EQ(mechanism.name, "CAM Cloud Chemistry"); + EXPECT_EQ(mechanism.species.size(), 10u); + EXPECT_EQ(mechanism.phases.size(), 2u); + + // One UNIFORM_SECTION representation. + ASSERT_EQ(mechanism.aerosol.representations.size(), 1u); + const auto& cloud = std::get(mechanism.aerosol.representations[0]); + EXPECT_EQ(cloud.name, "CLOUD"); + + // Processes: one reversible reaction followed by one dissolved reaction. + ASSERT_EQ(mechanism.aerosol.processes.size(), 2u); + const auto& reversible = std::get(mechanism.aerosol.processes[0]); + ASSERT_EQ(reversible.reactants.size(), 2u); + EXPECT_EQ(reversible.reactants[0].name, "HSO3m"); // components keyed on "name" + ASSERT_TRUE(reversible.equilibrium_constant.has_value()); + EXPECT_DOUBLE_EQ(reversible.equilibrium_constant->A, 1725.0); + + // Constraints: 3 Henry's-law equilibria + 3 dissolved equilibria + 4 linear constraints. + ASSERT_EQ(mechanism.aerosol.constraints.size(), 10u); + + // The first constraint is the SO2 Henry's-law equilibrium; its solvent properties are sourced + // from the species/phase definitions rather than the process block. + const auto& so2_equilibrium = std::get(mechanism.aerosol.constraints[0]); + EXPECT_EQ(so2_equilibrium.solvent, "H2O"); + EXPECT_DOUBLE_EQ(so2_equilibrium.solvent_molecular_weight, 0.01801); // from the species section + EXPECT_DOUBLE_EQ(so2_equilibrium.solvent_density, 997.0); // from the AQUEOUS phase + + // The first linear constraint's terms are keyed on "name". + const auto& linear = std::get(mechanism.aerosol.constraints[6]); + ASSERT_FALSE(linear.terms.empty()); + EXPECT_EQ(linear.terms[0].phase, "gas"); + EXPECT_EQ(linear.terms[0].name, "SO2"); +} From 8d72d0f49bcf5844b423a7918829b69d1ba671dd Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Thu, 25 Jun 2026 22:33:14 -0600 Subject: [PATCH 14/23] remove unit --- src/detail/v1/aerosol_keys.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detail/v1/aerosol_keys.hpp b/src/detail/v1/aerosol_keys.hpp index 840e0cdf..b7685249 100644 --- a/src/detail/v1/aerosol_keys.hpp +++ b/src/detail/v1/aerosol_keys.hpp @@ -25,7 +25,7 @@ namespace mechanism_configuration::v1::keys inline constexpr std::string_view SingleMomentMode_key = "SINGLE_MOMENT_MODE"; inline constexpr std::string_view geometric_mean_radius = "geometric mean radius [m]"; - inline constexpr std::string_view geometric_standard_deviation = "geometric standard deviation [-]"; + inline constexpr std::string_view geometric_standard_deviation = "geometric standard deviation"; inline constexpr std::string_view TwoMomentMode_key = "TWO_MOMENT_MODE"; // also: geometric_standard_deviation From a904c8708b13153d1fd48e00b2f10aa69ce3d0fe Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 14:40:41 -0600 Subject: [PATCH 15/23] add validation for aerosol --- src/validate.cpp | 170 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/src/validate.cpp b/src/validate.cpp index 84772969..f9ab88bd 100644 --- a/src/validate.cpp +++ b/src/validate.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace mechanism_configuration @@ -109,6 +110,44 @@ namespace mechanism_configuration } } + // ---- Aerosol Representations -------------------------------------------------------------------------- + for (const auto& representation : input.aerosol_representations) + { + for (const auto& phase : representation.phases) + { + if (!phase_species.contains(phase.name)) + errors.push_back( + { ErrorCode::UnknownPhase, + Message( + phase.location, + mc_fmt::format( + "Unknown phase '{}' in '{}' aerosol representation.", phase.name, representation.name)) }); + } + } + + // ---- Aerosol Processes -------------------------------------------------------------------------- + for (const auto& process : input.aerosol_processes) + { + for (const auto& species : process.species) + { + if (!species_names.contains(species.name)) + errors.push_back( + { ErrorCode::UnknownSpecies, + Message( + species.location, + mc_fmt::format("Unknown species '{}' in '{}' aerosol process.", species.name, process.type)) }); + } + for (const auto& phase : process.phases) + { + if (!phase_species.contains(phase.name)) + errors.push_back( + { ErrorCode::UnknownPhase, + Message( + phase.location, + mc_fmt::format("Unknown phase '{}' in '{}' aerosol process.", phase.name, process.type)) }); + } + } + return errors; } @@ -149,6 +188,7 @@ namespace mechanism_configuration input.phases.push_back(std::move(pr)); } + // ---- Reactions -------------------------------------------------------------------------- const auto& r = mechanism.reactions; auto add = [&](std::string_view type, const std::string& phase, @@ -185,6 +225,134 @@ namespace mechanism_configuration add("BRANCHED", x.gas_phase, Refs(x.reactants), Refs(products)); } - return ValidateSemantics(input); + // ---- Aerosol Representations -------------------------------------------------------------------------- + const auto& a = mechanism.aerosol; + for (const auto& x : a.representations) + { + std::visit( + [&](const auto& rep) + { + using T = std::decay_t; + std::string_view type; + if constexpr (std::is_same_v) + type = "UNIFORM_SECTION"; + else if constexpr (std::is_same_v) + type = "SINGLE_MOMENT_MODE"; + else if constexpr (std::is_same_v) + type = "TWO_MOMENT_MODE"; + + semantics::AerosolRepresentationRef ref{ std::string(type), rep.name, {}, std::nullopt }; + for (const auto& phase : rep.phases) + ref.phases.push_back({ phase, std::nullopt }); + input.aerosol_representations.push_back(std::move(ref)); + }, + x); + } + + // ---- Aerosol Processes -------------------------------------------------------------------------- + for (const auto& x : a.processes) + { + std::visit( + [&](const auto& proc) + { + using T = std::decay_t; + semantics::AerosolProcessRef ref; + + if constexpr (std::is_same_v) + { + ref.type = "DISSOLVED_REACTION"; + ref.phases.push_back({ proc.phase, std::nullopt }); + ref.species.push_back({ proc.solvent, std::nullopt }); + for (const auto& r : proc.reactants) + ref.species.push_back({ r.name, std::nullopt }); + for (const auto& p : proc.products) + ref.species.push_back({ p.name, std::nullopt }); + std::vector rep_refs; + for (const auto& [rep_name, _] : proc.rate_constants) + rep_refs.push_back({ rep_name, std::nullopt }); + ref.aerosol_representations = std::move(rep_refs); + } + else if constexpr (std::is_same_v) + { + ref.type = "DISSOLVED_REVERSIBLE_REACTION"; + ref.phases.push_back({ proc.phase, std::nullopt }); + ref.species.push_back({ proc.solvent, std::nullopt }); + for (const auto& r : proc.reactants) + ref.species.push_back({ r.name, std::nullopt }); + for (const auto& p : proc.products) + ref.species.push_back({ p.name, std::nullopt }); + std::vector rep_refs; + for (const auto& [rep_name, _] : proc.forward_rate_constants) + rep_refs.push_back({ rep_name, std::nullopt }); + for (const auto& [rep_name, _] : proc.reverse_rate_constants) + rep_refs.push_back({ rep_name, std::nullopt }); + ref.aerosol_representations = std::move(rep_refs); + } + else if constexpr (std::is_same_v) + { + ref.type = "HENRY_LAW_PHASE_TRANSFER"; + ref.phases.push_back({ proc.gas_phase, std::nullopt }); + ref.phases.push_back({ proc.condensed_phase, std::nullopt }); + ref.species.push_back({ proc.gas_species, std::nullopt }); + ref.species.push_back({ proc.condensed_species, std::nullopt }); + ref.species.push_back({ proc.solvent, std::nullopt }); + } + + input.aerosol_processes.push_back(std::move(ref)); + }, + x); + } + + // ---- Aerosol Constraints -------------------------------------------------------------------------- + for (const auto& x : a.constraints) + { + std::visit( + [&](const auto& con) + { + using T = std::decay_t; + semantics::AerosolProcessRef ref; + + if constexpr (std::is_same_v) + { + ref.type = "HENRY_LAW_EQUILIBRIUM"; + ref.phases.push_back({ con.gas_phase, std::nullopt }); + ref.phases.push_back({ con.condensed_phase, std::nullopt }); + ref.species.push_back({ con.gas_species, std::nullopt }); + ref.species.push_back({ con.condensed_species, std::nullopt }); + ref.species.push_back({ con.solvent, std::nullopt }); + } + else if constexpr (std::is_same_v) + { + ref.type = "DISSOLVED_EQUILIBRIUM"; + ref.phases.push_back({ con.phase, std::nullopt }); + ref.species.push_back({ con.algebraic_species, std::nullopt }); + ref.species.push_back({ con.solvent, std::nullopt }); + for (const auto& r : con.reactants) + ref.species.push_back({ r.name, std::nullopt }); + for (const auto& p : con.products) + ref.species.push_back({ p.name, std::nullopt }); + } + else if constexpr (std::is_same_v) + { + ref.type = "LINEAR_CONSTRAINT"; + ref.phases.push_back({ con.algebraic_phase, std::nullopt }); + ref.species.push_back({ con.algebraic_species, std::nullopt }); + for (const auto& term : con.terms) + { + ref.phases.push_back({ term.phase, std::nullopt }); + ref.species.push_back({ term.name, std::nullopt }); + } + } + + input.aerosol_processes.push_back(std::move(ref)); + }, + x); + } + + Errors errors = ValidateSemantics(input); + // Errors aerosol_errors = ValidateAerosol(mechanism); + // errors.insert(errors.end(), aerosol_errors.begin(), aerosol_errors.end()); + return errors; } + } // namespace mechanism_configuration From 26edebdcf16f8e208b6567fff71a5c5bf0549add Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 14:42:44 -0600 Subject: [PATCH 16/23] flattening aerosol into semantics input --- src/validate.cpp | 135 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/src/validate.cpp b/src/validate.cpp index f9ab88bd..d0d13475 100644 --- a/src/validate.cpp +++ b/src/validate.cpp @@ -151,6 +151,137 @@ namespace mechanism_configuration return errors; } + Errors ValidateAerosol(const Mechanism& mechanism) + { + Errors errors; + const auto& aerosol = mechanism.aerosol; + if (aerosol.processes.empty() && aerosol.constraints.empty()) + return errors; + + // Index species (for molecular weight) and phase-species (for membership / diffusion / density). + std::unordered_map species_index; + for (const auto& s : mechanism.species) + species_index.emplace(s.name, &s); + + std::unordered_map> phase_index; + for (const auto& phase : mechanism.phases) + { + auto& registered = phase_index[phase.name]; + for (const auto& ps : phase.species) + registered.emplace(ps.name, &ps); + } + + // Verifies that `species` is registered in `phase`; returns the entry (or nullptr) and reports. + auto require_membership = + [&](const std::string& phase, const std::string& species, const std::string& context) -> const types::PhaseSpecies* + { + const auto phase_it = phase_index.find(phase); + if (phase_it == phase_index.end()) + { + errors.push_back({ ErrorCode::UnknownPhase, + Message(std::nullopt, mc_fmt::format("Unknown phase '{}' referenced by {}.", phase, context)) }); + return nullptr; + } + const auto species_it = phase_it->second.find(species); + if (species_it == phase_it->second.end()) + { + errors.push_back({ ErrorCode::RequestedSpeciesNotRegisteredInPhase, + Message(std::nullopt, + mc_fmt::format( + "Species '{}' ({}) is not defined in the '{}' phase.", species, context, phase)) }); + return nullptr; + } + return species_it->second; + }; + + auto require_diffusion = [&](const std::string& phase, const std::string& species, const std::string& context) + { + const auto* entry = require_membership(phase, species, context); + if (entry && !entry->diffusion_coefficient) + errors.push_back({ ErrorCode::RequiredKeyNotFound, + Message(std::nullopt, + mc_fmt::format("{}: gas-phase species '{}' has no diffusion coefficient defined in " + "the '{}' phase.", + context, species, phase)) }); + }; + + auto require_density = [&](const std::string& phase, const std::string& species, const std::string& context) + { + const auto* entry = require_membership(phase, species, context); + if (entry && !entry->density) + errors.push_back({ ErrorCode::RequiredKeyNotFound, + Message(std::nullopt, + mc_fmt::format("{}: solvent species '{}' has no density defined in the '{}' phase.", + context, species, phase)) }); + }; + + auto require_molecular_weight = [&](const std::string& species, const std::string& context) + { + const auto species_it = species_index.find(species); + if (species_it == species_index.end()) + errors.push_back({ ErrorCode::UnknownSpecies, + Message(std::nullopt, mc_fmt::format("Unknown species '{}' referenced by {}.", species, context)) }); + else if (!species_it->second->molecular_weight) + errors.push_back({ ErrorCode::RequiredKeyNotFound, + Message(std::nullopt, + mc_fmt::format("{}: solvent species '{}' has no molecular weight defined.", context, species)) }); + }; + + for (const auto& process : aerosol.processes) + { + if (const auto* p = std::get_if(&process)) + { + for (const auto& c : p->reactants) + require_membership(p->phase, c.name, "DISSOLVED_REACTION reactant"); + for (const auto& c : p->products) + require_membership(p->phase, c.name, "DISSOLVED_REACTION product"); + require_membership(p->phase, p->solvent, "DISSOLVED_REACTION solvent"); + } + else if (const auto* p = std::get_if(&process)) + { + for (const auto& c : p->reactants) + require_membership(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION reactant"); + for (const auto& c : p->products) + require_membership(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION product"); + require_membership(p->phase, p->solvent, "DISSOLVED_REVERSIBLE_REACTION solvent"); + } + else if (const auto* p = std::get_if(&process)) + { + require_membership(p->condensed_phase, p->condensed_species, "HENRY_LAW_PHASE_TRANSFER condensed species"); + require_membership(p->condensed_phase, p->solvent, "HENRY_LAW_PHASE_TRANSFER solvent"); + require_diffusion(p->gas_phase, p->gas_species, "HENRY_LAW_PHASE_TRANSFER"); + } + } + + for (const auto& constraint : aerosol.constraints) + { + if (const auto* c = std::get_if(&constraint)) + { + require_membership(c->gas_phase, c->gas_species, "HENRY_LAW_EQUILIBRIUM gas species"); + require_membership(c->condensed_phase, c->condensed_species, "HENRY_LAW_EQUILIBRIUM condensed species"); + require_density(c->condensed_phase, c->solvent, "HENRY_LAW_EQUILIBRIUM"); + require_molecular_weight(c->solvent, "HENRY_LAW_EQUILIBRIUM"); + } + else if (const auto* c = std::get_if(&constraint)) + { + require_membership(c->phase, c->algebraic_species, "DISSOLVED_EQUILIBRIUM algebraic species"); + require_membership(c->phase, c->solvent, "DISSOLVED_EQUILIBRIUM solvent"); + for (const auto& x : c->reactants) + require_membership(c->phase, x.name, "DISSOLVED_EQUILIBRIUM reactant"); + for (const auto& x : c->products) + require_membership(c->phase, x.name, "DISSOLVED_EQUILIBRIUM product"); + } + else if (const auto* c = std::get_if(&constraint)) + { + require_membership(c->algebraic_phase, c->algebraic_species, "LINEAR_CONSTRAINT algebraic species"); + for (const auto& t : c->terms) + require_membership(t.phase, t.name, "LINEAR_CONSTRAINT term"); + } + } + + return errors; + } + namespace { // Builds a location-free list of component references from a struct component list. @@ -350,8 +481,8 @@ namespace mechanism_configuration } Errors errors = ValidateSemantics(input); - // Errors aerosol_errors = ValidateAerosol(mechanism); - // errors.insert(errors.end(), aerosol_errors.begin(), aerosol_errors.end()); + Errors aerosol_errors = ValidateAerosol(mechanism); + errors.insert(errors.end(), aerosol_errors.begin(), aerosol_errors.end()); return errors; } From c334c5a9905fe944ece7c4437dbfe1614d978082 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 15:23:15 -0600 Subject: [PATCH 17/23] density should be onwed by species, not phase species --- examples/v1/cam_cloud_chemistry.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/v1/cam_cloud_chemistry.json b/examples/v1/cam_cloud_chemistry.json index 0755db00..8ffddb7e 100644 --- a/examples/v1/cam_cloud_chemistry.json +++ b/examples/v1/cam_cloud_chemistry.json @@ -12,7 +12,7 @@ { "name": "SO3mm", "molecular weight [kg mol-1]": 0.08007 }, { "name": "SO4mm", "molecular weight [kg mol-1]": 0.09607 }, { "name": "SO2OOHm", "molecular weight [kg mol-1]": 0.11307 }, - { "name": "H2O", "molecular weight [kg mol-1]": 0.01801 } + { "name": "H2O", "molecular weight [kg mol-1]": 0.01801, "density [kg m-3]": 997.0 } ], "phases": [ @@ -27,8 +27,15 @@ { "name": "AQUEOUS", "species": [ - "SO2", "H2O2", "O3", "Hp", "OHm", "HSO3m", "SO3mm", "SO4mm", "SO2OOHm", - { "name": "H2O", "density [kg m-3]": 997.0 } + { "name": "SO2" }, + { "name": "H2O2" }, + { "name": "O3" }, + { "name": "Hp" }, + { "name": "OHm" }, + { "name": "HSO3m" }, + { "name": "SO3mm" }, + { "name": "SO4mm" }, + { "name": "SO2OOHm" } ] } ], From 15180e90716d990a2f82d7ee354a22f81052f471 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 16:08:54 -0600 Subject: [PATCH 18/23] differentiate validate gas/aerosol --- examples/v1/cam_cloud_chemistry.json | 13 +++---------- src/v1/aerosol_type_parsers.cpp | 12 +++++++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/examples/v1/cam_cloud_chemistry.json b/examples/v1/cam_cloud_chemistry.json index 8ffddb7e..0755db00 100644 --- a/examples/v1/cam_cloud_chemistry.json +++ b/examples/v1/cam_cloud_chemistry.json @@ -12,7 +12,7 @@ { "name": "SO3mm", "molecular weight [kg mol-1]": 0.08007 }, { "name": "SO4mm", "molecular weight [kg mol-1]": 0.09607 }, { "name": "SO2OOHm", "molecular weight [kg mol-1]": 0.11307 }, - { "name": "H2O", "molecular weight [kg mol-1]": 0.01801, "density [kg m-3]": 997.0 } + { "name": "H2O", "molecular weight [kg mol-1]": 0.01801 } ], "phases": [ @@ -27,15 +27,8 @@ { "name": "AQUEOUS", "species": [ - { "name": "SO2" }, - { "name": "H2O2" }, - { "name": "O3" }, - { "name": "Hp" }, - { "name": "OHm" }, - { "name": "HSO3m" }, - { "name": "SO3mm" }, - { "name": "SO4mm" }, - { "name": "SO2OOHm" } + "SO2", "H2O2", "O3", "Hp", "OHm", "HSO3m", "SO3mm", "SO4mm", "SO2OOHm", + { "name": "H2O", "density [kg m-3]": 997.0 } ] } ], diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index 24ea6b96..630949ee 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -149,9 +149,10 @@ namespace mechanism_configuration transfer.condensed_species = object[keys::condensed_phase_species].as(); transfer.solvent = object[keys::solvent].as(); transfer.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); - // Sourced from the gas-phase species' definition. Schema validation guarantees it is present. + // Sourced from the gas-phase species' definition; presence is enforced by ValidateAerosol, + // which runs before this result is returned, so a missing value defaults harmlessly here. transfer.diffusion_coefficient = - FindPhaseSpeciesDiffusionCoefficient(phases, transfer.gas_phase, transfer.gas_species).value(); + FindPhaseSpeciesDiffusionCoefficient(phases, transfer.gas_phase, transfer.gas_species).value_or(0.0); transfer.accommodation_coefficient = object[keys::accommodation_coefficient].as(); return transfer; @@ -210,10 +211,11 @@ namespace mechanism_configuration equilibrium.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); // Sourced from the solvent species' definition: molecular weight from the species section, - // density from the condensed phase. Schema validation guarantees both are present. - equilibrium.solvent_molecular_weight = FindSpeciesMolecularWeight(species, equilibrium.solvent).value(); + // density from the condensed phase. Presence is enforced by ValidateAerosol, which runs + // before this result is returned, so a missing value defaults harmlessly here. + equilibrium.solvent_molecular_weight = FindSpeciesMolecularWeight(species, equilibrium.solvent).value_or(0.0); equilibrium.solvent_density = - FindPhaseSpeciesDensity(phases, equilibrium.condensed_phase, equilibrium.solvent).value(); + FindPhaseSpeciesDensity(phases, equilibrium.condensed_phase, equilibrium.solvent).value_or(0.0); return equilibrium; } From 93366597213d736822ac0799708f1cf185983af9 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 16:46:57 -0600 Subject: [PATCH 19/23] code clean up after adding validation for aerosol --- include/mechanism_configuration/errors.hpp | 1 + include/mechanism_configuration/validate.hpp | 43 +- src/detail/v1/aerosol_type_schema.hpp | 15 +- src/errors.cpp | 1 + src/v1/aerosol_type_parsers.cpp | 4 +- src/v1/aerosol_type_schema.cpp | 60 +-- src/v1/parser.cpp | 18 +- src/validate.cpp | 402 +++++++------------ 8 files changed, 200 insertions(+), 344 deletions(-) diff --git a/include/mechanism_configuration/errors.hpp b/include/mechanism_configuration/errors.hpp index a838f9ea..423c42a5 100644 --- a/include/mechanism_configuration/errors.hpp +++ b/include/mechanism_configuration/errors.hpp @@ -44,6 +44,7 @@ namespace mechanism_configuration ReactionRequiresUnknownSpecies, UnknownSpecies, UnknownPhase, + UnknownAerosolRepresentation, RequestedSpeciesNotRegisteredInPhase, TooManyReactionComponents, InvalidVersion, diff --git a/include/mechanism_configuration/validate.hpp b/include/mechanism_configuration/validate.hpp index c603271a..adc84eb9 100644 --- a/include/mechanism_configuration/validate.hpp +++ b/include/mechanism_configuration/validate.hpp @@ -13,10 +13,15 @@ namespace mechanism_configuration { - // Intermediate over which all semantic validation runs. Both a parsed document and an - // in-code Mechanism produce one of these and hand it to ValidateSemantics, so the rules - // live in exactly one place. A parsed document fills in source locations (so errors carry - // line:col); an in-code Mechanism leaves them empty. The domain types stay location-free. + + // Intermediate over which the core semantic validation (species, phases, gas-phase reactions) + // runs. Both a parsed document and an in-code Mechanism lower into one of these and hand it to + // ValidateSemantics, so those rules live in exactly one place. A parsed document fills in source + // locations (so errors carry line:col); an in-code Mechanism leaves them empty. + // + // Aerosol cross-references are validated separately by ValidateAerosolModel, which works on the + // domain structs directly because it needs phase membership and optional-property values that + // this name-only view cannot carry. namespace semantics { struct NamedRef @@ -47,17 +52,31 @@ namespace mechanism_configuration std::vector phases; std::vector reactions; }; + } // namespace semantics - /// @brief The single home for the mechanism's semantic rules. Errors include `line:col` - /// for any element whose source location was supplied. + /// @brief The single home for the core (species / phase / gas-phase reaction) semantic rules. + /// Errors include `line:col` for any element whose source location was supplied. Errors ValidateSemantics(const semantics::Input& input); - /// @brief Validates the semantic invariants of a canonical Mechanism, independent of how it - /// was produced (parsed from any version, or constructed in code). Convenience wrapper - /// that builds a location-free semantics::Input and calls ValidateSemantics. - /// - /// Structural/deserialization concerns (YAML keys, types, formatting) are the responsibility - /// of the version-specific parsers and are not repeated here. +/// @brief Validates a Mechanism's species, phases, and gas-phase reactions by +/// converting them to a location-free semantics::Input and running +/// ValidateSemantics. + Errors ValidateGasModel(const Mechanism& mechanism); + +/// @brief Validates aerosol cross-references against the mechanism's species and phases. +/// +/// Checks phase/species references, representation-keyed rate-constant maps, and +/// required definition-derived properties. These validations require full domain +/// information unavailable to ValidateSemantics. Returns no errors if the mechanism +/// has no aerosol section. + Errors ValidateAerosolModel(const Mechanism& mechanism); + +/// @brief Validates the semantic invariants of a canonical Mechanism, regardless of whether it +/// was parsed or constructed in code. Combines ValidateGasModel and +/// ValidateAerosolModel, returning all validation errors. +/// +/// Excludes structural/deserialization validation, which is handled by version-specific parsers. Errors Validate(const Mechanism& mechanism); + } // namespace mechanism_configuration diff --git a/src/detail/v1/aerosol_type_schema.hpp b/src/detail/v1/aerosol_type_schema.hpp index 5f517405..dd522af6 100644 --- a/src/detail/v1/aerosol_type_schema.hpp +++ b/src/detail/v1/aerosol_type_schema.hpp @@ -5,12 +5,9 @@ #pragma once #include -#include #include -#include - namespace mechanism_configuration::v1 { /// @brief Schema-validates each entry of the aerosol representations section. @@ -19,16 +16,10 @@ namespace mechanism_configuration::v1 Errors CheckAerosolRepresentationsSchema(const YAML::Node& representations_list); /// @brief Schema-validates each entry of the aerosol processes section which mixes - /// process and constraint types. + /// process and constraint types. Structural only; cross-references against species and + /// phases (membership, sourced properties) are checked separately by ValidateAerosolModel. /// @param processes_list YAML node containing the process/constraint entries - /// @param species Parsed top-level species, used to validate cross-references such as a Henry's-law - /// equilibrium's solvent species carrying a molecular weight - /// @param phases Parsed phases, used to validate cross-references such as a phase-transfer's - /// gas-phase species carrying a diffusion coefficient, or a solvent's density /// @return List of structural errors, or empty if all entries conform - Errors CheckAerosolProcessesSchema( - const YAML::Node& processes_list, - const std::vector& species, - const std::vector& phases); + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list); } // namespace mechanism_configuration::v1 diff --git a/src/errors.cpp b/src/errors.cpp index 49875267..795e86f7 100644 --- a/src/errors.cpp +++ b/src/errors.cpp @@ -25,6 +25,7 @@ namespace mechanism_configuration case ErrorCode::ReactionRequiresUnknownSpecies: return "ReactionRequiresUnknownSpecies"; case ErrorCode::UnknownSpecies: return "UnknownSpecies"; case ErrorCode::UnknownPhase: return "UnknownPhase"; + case ErrorCode::UnknownAerosolRepresentation: return "UnknownAerosolRepresentation"; case ErrorCode::RequestedSpeciesNotRegisteredInPhase: return "RequestedSpeciesNotRegisteredInPhase"; case ErrorCode::TooManyReactionComponents: return "TooManyReactionComponents"; case ErrorCode::InvalidVersion: return "InvalidVersion"; diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index 630949ee..b6ebe35e 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -149,7 +149,7 @@ namespace mechanism_configuration transfer.condensed_species = object[keys::condensed_phase_species].as(); transfer.solvent = object[keys::solvent].as(); transfer.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); - // Sourced from the gas-phase species' definition; presence is enforced by ValidateAerosol, + // Sourced from the gas-phase species' definition; presence is enforced by ValidateAerosolModel, // which runs before this result is returned, so a missing value defaults harmlessly here. transfer.diffusion_coefficient = FindPhaseSpeciesDiffusionCoefficient(phases, transfer.gas_phase, transfer.gas_species).value_or(0.0); @@ -211,7 +211,7 @@ namespace mechanism_configuration equilibrium.henry_law_constant = ParseHenryLawConstant(object[keys::henry_law_constant]); // Sourced from the solvent species' definition: molecular weight from the species section, - // density from the condensed phase. Presence is enforced by ValidateAerosol, which runs + // density from the condensed phase. Presence is enforced by ValidateAerosolModel, which runs // before this result is returned, so a missing value defaults harmlessly here. equilibrium.solvent_molecular_weight = FindSpeciesMolecularWeight(species, equilibrium.solvent).value_or(0.0); equilibrium.solvent_density = diff --git a/src/v1/aerosol_type_schema.cpp b/src/v1/aerosol_type_schema.cpp index 381f4a08..ccd3274d 100644 --- a/src/v1/aerosol_type_schema.cpp +++ b/src/v1/aerosol_type_schema.cpp @@ -129,10 +129,7 @@ namespace mechanism_configuration return errors; } - Errors CheckAerosolProcessesSchema( - const YAML::Node& processes_list, - const std::vector& species, - const std::vector& phases) + Errors CheckAerosolProcessesSchema(const YAML::Node& processes_list) { Errors errors; @@ -155,8 +152,8 @@ namespace mechanism_configuration const std::string type = object[keys::type].as(); if (type == keys::HenryLawPhaseTransfer_key) { - // The diffusion coefficient is not given here; it is sourced from the gas-phase - // species' definition in the phases section. + // The diffusion coefficient is not given here. It is sourced from the gas-phase + // species' definition in the phases section (see ValidateAerosolModel). required_keys = { keys::type, keys::gas_phase, keys::gas_phase_species, @@ -167,26 +164,6 @@ namespace mechanism_configuration keys::accommodation_coefficient }; if (object[keys::henry_law_constant]) nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); - // The gas-phase species must carry a diffusion coefficient, since this process reads it - // from the phases section rather than from the process block itself. - if (object[keys::gas_phase] && object[keys::gas_phase_species]) - { - const auto gas_phase = object[keys::gas_phase].as(); - const auto gas_species = object[keys::gas_phase_species].as(); - if (!FindPhaseSpeciesDiffusionCoefficient(phases, gas_phase, gas_species).has_value()) - { - ErrorLocation error_location{ object.Mark().line, object.Mark().column }; - nested_errors.push_back( - { ErrorCode::RequiredKeyNotFound, - mc_fmt::format( - "{} error: '{}' references gas-phase species '{}' in phase '{}', which has no " - "diffusion coefficient defined in the phases section.", - error_location, - keys::HenryLawPhaseTransfer_key, - gas_species, - gas_phase) }); - } - } } else if (type == keys::DissolvedReaction_key) { @@ -241,8 +218,7 @@ namespace mechanism_configuration else if (type == keys::HenryLawEquilibrium_key) { // The solvent's molecular weight and density are not given here; they are sourced from - // the solvent species' definition (molecular weight from the species section, density - // from the condensed phase). + // the solvent species' definition (see ValidateAerosolModel). required_keys = { keys::type, keys::gas_phase, keys::gas_phase_species, @@ -252,34 +228,6 @@ namespace mechanism_configuration keys::henry_law_constant }; if (object[keys::henry_law_constant]) nested_errors = CheckHenryLawConstantSchema(object[keys::henry_law_constant]); - if (object[keys::solvent] && object[keys::condensed_phase]) - { - const auto solvent = object[keys::solvent].as(); - const auto condensed_phase = object[keys::condensed_phase].as(); - ErrorLocation error_location{ object.Mark().line, object.Mark().column }; - if (!FindSpeciesMolecularWeight(species, solvent).has_value()) - { - nested_errors.push_back( - { ErrorCode::RequiredKeyNotFound, - mc_fmt::format( - "{} error: '{}' solvent species '{}' has no molecular weight defined in the " - "species section.", - error_location, - keys::HenryLawEquilibrium_key, - solvent) }); - } - if (!FindPhaseSpeciesDensity(phases, condensed_phase, solvent).has_value()) - { - nested_errors.push_back( - { ErrorCode::RequiredKeyNotFound, - mc_fmt::format( - "{} error: '{}' solvent species '{}' has no density defined in phase '{}'.", - error_location, - keys::HenryLawEquilibrium_key, - solvent, - condensed_phase) }); - } - } } else if (type == keys::DissolvedEquilibrium_key) { diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index e60140a5..ba8b615b 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -324,7 +325,7 @@ namespace mechanism_configuration::v1 errors.insert(errors.end(), schema_errors.begin(), schema_errors.end()); } - schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes], parsed_species, parsed_phases); + schema_errors = CheckAerosolProcessesSchema(object[keys::aerosol_processes]); if (!schema_errors.empty()) { AppendFilePath(config_path_, schema_errors); @@ -389,8 +390,19 @@ namespace mechanism_configuration::v1 return std::unexpected(std::move(errors)); } - // 3) Build the Mechanism (only reached when fully valid). - return Build(object); + // 3) Build the Mechanism (structurally valid; aerosol parsers source values defensively). + Mechanism mechanism = Build(object); + + // 4) Aerosol cross-reference validation runs against the built structs (species/phase + // membership and sourced properties such as diffusion coefficient, solvent density). + Errors aerosol_errors = ValidateAerosolModel(mechanism); + if (!aerosol_errors.empty()) + { + AppendFilePath(config_path_, aerosol_errors); + return std::unexpected(std::move(aerosol_errors)); + } + + return mechanism; } catch (const std::exception& e) { diff --git a/src/validate.cpp b/src/validate.cpp index d0d13475..fe33f31b 100644 --- a/src/validate.cpp +++ b/src/validate.cpp @@ -110,59 +110,99 @@ namespace mechanism_configuration } } - // ---- Aerosol Representations -------------------------------------------------------------------------- - for (const auto& representation : input.aerosol_representations) + return errors; + } + + namespace + { + // Builds a location-free list of component references from a struct component list. + std::vector Refs(const std::vector& components) { - for (const auto& phase : representation.phases) - { - if (!phase_species.contains(phase.name)) - errors.push_back( - { ErrorCode::UnknownPhase, - Message( - phase.location, - mc_fmt::format( - "Unknown phase '{}' in '{}' aerosol representation.", phase.name, representation.name)) }); - } + std::vector refs; + refs.reserve(components.size()); + for (const auto& c : components) + refs.push_back({ c.name, std::nullopt }); + return refs; } - // ---- Aerosol Processes -------------------------------------------------------------------------- - for (const auto& process : input.aerosol_processes) + semantics::ReactionRef ReactionRefOf( + std::string_view type, + const std::string& phase, + std::vector reactants, + std::vector products) { - for (const auto& species : process.species) - { - if (!species_names.contains(species.name)) - errors.push_back( - { ErrorCode::UnknownSpecies, - Message( - species.location, - mc_fmt::format("Unknown species '{}' in '{}' aerosol process.", species.name, process.type)) }); - } - for (const auto& phase : process.phases) - { - if (!phase_species.contains(phase.name)) - errors.push_back( - { ErrorCode::UnknownPhase, - Message( - phase.location, - mc_fmt::format("Unknown phase '{}' in '{}' aerosol process.", phase.name, process.type)) }); - } + return semantics::ReactionRef{ std::string(type), phase, std::move(reactants), std::move(products), std::nullopt }; } + } // namespace - return errors; + Errors ValidateGasModel(const Mechanism& mechanism) + { + semantics::Input input; + + for (const auto& s : mechanism.species) + input.species.push_back({ s.name, std::nullopt }); + + for (const auto& phase : mechanism.phases) + { + semantics::PhaseRef pr{ phase.name, {}, std::nullopt }; + for (const auto& ps : phase.species) + pr.species.push_back({ ps.name, std::nullopt }); + input.phases.push_back(std::move(pr)); + } + + const auto& r = mechanism.reactions; + auto add = [&](std::string_view type, + const std::string& phase, + std::vector reactants, + std::vector products) + { input.reactions.push_back(ReactionRefOf(type, phase, std::move(reactants), std::move(products))); }; + + for (const auto& x : r.arrhenius) + add("ARRHENIUS", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.troe) + add("TROE", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.ternary_chemical_activation) + add("TERNARY_CHEMICAL_ACTIVATION", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.tunneling) + add("TUNNELING", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.taylor_series) + add("TAYLOR_SERIES", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.user_defined) + add("USER_DEFINED", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.lambda_rate_constant) + add("LAMBDA_RATE_CONSTANT", x.gas_phase, Refs(x.reactants), Refs(x.products)); + for (const auto& x : r.emission) + add("EMISSION", x.gas_phase, {}, Refs(x.products)); + for (const auto& x : r.first_order_loss) + add("FIRST_ORDER_LOSS", x.gas_phase, Refs({ x.reactants }), Refs(x.products)); + for (const auto& x : r.photolysis) + add("PHOTOLYSIS", x.gas_phase, Refs({ x.reactants }), Refs(x.products)); + for (const auto& x : r.surface) + add("SURFACE", x.gas_phase, Refs({ x.gas_phase_species }), Refs(x.gas_phase_products)); + for (const auto& x : r.branched) + { + std::vector products = x.alkoxy_products; + products.insert(products.end(), x.nitrate_products.begin(), x.nitrate_products.end()); + add("BRANCHED", x.gas_phase, Refs(x.reactants), Refs(products)); + } + + return ValidateSemantics(input); } - Errors ValidateAerosol(const Mechanism& mechanism) + Errors ValidateAerosolModel(const Mechanism& mechanism) { Errors errors; + const auto& aerosol = mechanism.aerosol; - if (aerosol.processes.empty() && aerosol.constraints.empty()) + if (aerosol.representations.empty() && aerosol.processes.empty() && aerosol.constraints.empty()) return errors; - // Index species (for molecular weight) and phase-species (for membership / diffusion / density). + // Index species for molecular weight. std::unordered_map species_index; for (const auto& s : mechanism.species) species_index.emplace(s.name, &s); + // Index phase-species for membership, diffusion, density. std::unordered_map> phase_index; for (const auto& phase : mechanism.phases) { @@ -171,8 +211,13 @@ namespace mechanism_configuration registered.emplace(ps.name, &ps); } - // Verifies that `species` is registered in `phase`; returns the entry (or nullptr) and reports. - auto require_membership = + // Rate-constants are keyed by its aerosol representation + std::unordered_set representation_names; + for (const auto& representation : aerosol.representations) + std::visit([&](const auto& rep) { representation_names.insert(rep.name); }, representation); + + // Verifies that species is registered in phase. Returns the entry (or nullptr) and reports. + auto require_registered_species = [&](const std::string& phase, const std::string& species, const std::string& context) -> const types::PhaseSpecies* { const auto phase_it = phase_index.find(phase); @@ -196,22 +241,22 @@ namespace mechanism_configuration auto require_diffusion = [&](const std::string& phase, const std::string& species, const std::string& context) { - const auto* entry = require_membership(phase, species, context); + const auto* entry = require_registered_species(phase, species, context); if (entry && !entry->diffusion_coefficient) errors.push_back({ ErrorCode::RequiredKeyNotFound, Message(std::nullopt, - mc_fmt::format("{}: gas-phase species '{}' has no diffusion coefficient defined in " + mc_fmt::format("{}: species '{}' has no diffusion coefficient defined in " "the '{}' phase.", context, species, phase)) }); }; auto require_density = [&](const std::string& phase, const std::string& species, const std::string& context) { - const auto* entry = require_membership(phase, species, context); + const auto* entry = require_registered_species(phase, species, context); if (entry && !entry->density) errors.push_back({ ErrorCode::RequiredKeyNotFound, Message(std::nullopt, - mc_fmt::format("{}: solvent species '{}' has no density defined in the '{}' phase.", + mc_fmt::format("{}: species '{}' has no density defined in the '{}' phase.", context, species, phase)) }); }; @@ -224,31 +269,66 @@ namespace mechanism_configuration else if (!species_it->second->molecular_weight) errors.push_back({ ErrorCode::RequiredKeyNotFound, Message(std::nullopt, - mc_fmt::format("{}: solvent species '{}' has no molecular weight defined.", context, species)) }); + mc_fmt::format("{}: species '{}' has no molecular weight defined.", context, species)) }); + }; + + // Verifies that phase exists. + auto require_phase = [&](const std::string& phase, const std::string& context) + { + if (!phase_index.contains(phase)) + errors.push_back({ ErrorCode::UnknownPhase, + Message(std::nullopt, mc_fmt::format("Unknown phase '{}' referenced by {}.", phase, context)) }); + }; + + // Validates that each rate-constant map entry is keyed by a declared aerosol representation. + auto require_representation = [&](const std::string& representation, const std::string& context) + { + if (!representation_names.contains(representation)) + errors.push_back({ ErrorCode::UnknownAerosolRepresentation, + Message(std::nullopt, + mc_fmt::format( + "Unknown aerosol representation '{}' referenced by {}.", representation, context)) }); }; + for (const auto& representation : aerosol.representations) + { + std::visit( + [&](const auto& rep) + { + for (const auto& phase : rep.phases) + require_phase(phase, mc_fmt::format("aerosol representation '{}'", rep.name)); + }, + representation); + } + for (const auto& process : aerosol.processes) { if (const auto* p = std::get_if(&process)) { for (const auto& c : p->reactants) - require_membership(p->phase, c.name, "DISSOLVED_REACTION reactant"); + require_registered_species(p->phase, c.name, "DISSOLVED_REACTION reactant"); for (const auto& c : p->products) - require_membership(p->phase, c.name, "DISSOLVED_REACTION product"); - require_membership(p->phase, p->solvent, "DISSOLVED_REACTION solvent"); + require_registered_species(p->phase, c.name, "DISSOLVED_REACTION product"); + require_registered_species(p->phase, p->solvent, "DISSOLVED_REACTION solvent"); + for (const auto& entry : p->rate_constants) + require_representation(entry.first, "DISSOLVED_REACTION rate constant"); } else if (const auto* p = std::get_if(&process)) { for (const auto& c : p->reactants) - require_membership(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION reactant"); + require_registered_species(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION reactant"); for (const auto& c : p->products) - require_membership(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION product"); - require_membership(p->phase, p->solvent, "DISSOLVED_REVERSIBLE_REACTION solvent"); + require_registered_species(p->phase, c.name, "DISSOLVED_REVERSIBLE_REACTION product"); + require_registered_species(p->phase, p->solvent, "DISSOLVED_REVERSIBLE_REACTION solvent"); + for (const auto& entry : p->forward_rate_constants) + require_representation(entry.first, "DISSOLVED_REVERSIBLE_REACTION forward rate constant"); + for (const auto& entry : p->reverse_rate_constants) + require_representation(entry.first, "DISSOLVED_REVERSIBLE_REACTION reverse rate constant"); } else if (const auto* p = std::get_if(&process)) { - require_membership(p->condensed_phase, p->condensed_species, "HENRY_LAW_PHASE_TRANSFER condensed species"); - require_membership(p->condensed_phase, p->solvent, "HENRY_LAW_PHASE_TRANSFER solvent"); + require_registered_species(p->condensed_phase, p->condensed_species, "HENRY_LAW_PHASE_TRANSFER condensed species"); + require_registered_species(p->condensed_phase, p->solvent, "HENRY_LAW_PHASE_TRANSFER solvent"); require_diffusion(p->gas_phase, p->gas_species, "HENRY_LAW_PHASE_TRANSFER"); } } @@ -257,231 +337,35 @@ namespace mechanism_configuration { if (const auto* c = std::get_if(&constraint)) { - require_membership(c->gas_phase, c->gas_species, "HENRY_LAW_EQUILIBRIUM gas species"); - require_membership(c->condensed_phase, c->condensed_species, "HENRY_LAW_EQUILIBRIUM condensed species"); - require_density(c->condensed_phase, c->solvent, "HENRY_LAW_EQUILIBRIUM"); - require_molecular_weight(c->solvent, "HENRY_LAW_EQUILIBRIUM"); + require_registered_species(c->gas_phase, c->gas_species, "HENRY_LAW_EQUILIBRIUM gas species"); + require_registered_species(c->condensed_phase, c->condensed_species, "HENRY_LAW_EQUILIBRIUM condensed species"); + require_density(c->condensed_phase, c->solvent, "HENRY_LAW_EQUILIBRIUM solvent"); + require_molecular_weight(c->solvent, "HENRY_LAW_EQUILIBRIUM solvent"); } else if (const auto* c = std::get_if(&constraint)) { - require_membership(c->phase, c->algebraic_species, "DISSOLVED_EQUILIBRIUM algebraic species"); - require_membership(c->phase, c->solvent, "DISSOLVED_EQUILIBRIUM solvent"); + require_registered_species(c->phase, c->algebraic_species, "DISSOLVED_EQUILIBRIUM algebraic species"); + require_registered_species(c->phase, c->solvent, "DISSOLVED_EQUILIBRIUM solvent"); for (const auto& x : c->reactants) - require_membership(c->phase, x.name, "DISSOLVED_EQUILIBRIUM reactant"); + require_registered_species(c->phase, x.name, "DISSOLVED_EQUILIBRIUM reactant"); for (const auto& x : c->products) - require_membership(c->phase, x.name, "DISSOLVED_EQUILIBRIUM product"); + require_registered_species(c->phase, x.name, "DISSOLVED_EQUILIBRIUM product"); } else if (const auto* c = std::get_if(&constraint)) { - require_membership(c->algebraic_phase, c->algebraic_species, "LINEAR_CONSTRAINT algebraic species"); + require_registered_species(c->algebraic_phase, c->algebraic_species, "LINEAR_CONSTRAINT algebraic species"); for (const auto& t : c->terms) - require_membership(t.phase, t.name, "LINEAR_CONSTRAINT term"); + require_registered_species(t.phase, t.name, "LINEAR_CONSTRAINT term"); } } return errors; } - namespace - { - // Builds a location-free list of component references from a struct component list. - std::vector Refs(const std::vector& components) - { - std::vector refs; - refs.reserve(components.size()); - for (const auto& c : components) - refs.push_back({ c.name, std::nullopt }); - return refs; - } - - semantics::ReactionRef ReactionRefOf( - std::string_view type, - const std::string& phase, - std::vector reactants, - std::vector products) - { - return semantics::ReactionRef{ std::string(type), phase, std::move(reactants), std::move(products), std::nullopt }; - } - } // namespace - Errors Validate(const Mechanism& mechanism) { - semantics::Input input; - - for (const auto& s : mechanism.species) - input.species.push_back({ s.name, std::nullopt }); - - for (const auto& phase : mechanism.phases) - { - semantics::PhaseRef pr{ phase.name, {}, std::nullopt }; - for (const auto& ps : phase.species) - pr.species.push_back({ ps.name, std::nullopt }); - input.phases.push_back(std::move(pr)); - } - - // ---- Reactions -------------------------------------------------------------------------- - const auto& r = mechanism.reactions; - auto add = [&](std::string_view type, - const std::string& phase, - std::vector reactants, - std::vector products) - { input.reactions.push_back(ReactionRefOf(type, phase, std::move(reactants), std::move(products))); }; - - for (const auto& x : r.arrhenius) - add("ARRHENIUS", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.troe) - add("TROE", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.ternary_chemical_activation) - add("TERNARY_CHEMICAL_ACTIVATION", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.tunneling) - add("TUNNELING", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.taylor_series) - add("TAYLOR_SERIES", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.user_defined) - add("USER_DEFINED", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.lambda_rate_constant) - add("LAMBDA_RATE_CONSTANT", x.gas_phase, Refs(x.reactants), Refs(x.products)); - for (const auto& x : r.emission) - add("EMISSION", x.gas_phase, {}, Refs(x.products)); - for (const auto& x : r.first_order_loss) - add("FIRST_ORDER_LOSS", x.gas_phase, Refs({ x.reactants }), Refs(x.products)); - for (const auto& x : r.photolysis) - add("PHOTOLYSIS", x.gas_phase, Refs({ x.reactants }), Refs(x.products)); - for (const auto& x : r.surface) - add("SURFACE", x.gas_phase, Refs({ x.gas_phase_species }), Refs(x.gas_phase_products)); - for (const auto& x : r.branched) - { - std::vector products = x.alkoxy_products; - products.insert(products.end(), x.nitrate_products.begin(), x.nitrate_products.end()); - add("BRANCHED", x.gas_phase, Refs(x.reactants), Refs(products)); - } - - // ---- Aerosol Representations -------------------------------------------------------------------------- - const auto& a = mechanism.aerosol; - for (const auto& x : a.representations) - { - std::visit( - [&](const auto& rep) - { - using T = std::decay_t; - std::string_view type; - if constexpr (std::is_same_v) - type = "UNIFORM_SECTION"; - else if constexpr (std::is_same_v) - type = "SINGLE_MOMENT_MODE"; - else if constexpr (std::is_same_v) - type = "TWO_MOMENT_MODE"; - - semantics::AerosolRepresentationRef ref{ std::string(type), rep.name, {}, std::nullopt }; - for (const auto& phase : rep.phases) - ref.phases.push_back({ phase, std::nullopt }); - input.aerosol_representations.push_back(std::move(ref)); - }, - x); - } - - // ---- Aerosol Processes -------------------------------------------------------------------------- - for (const auto& x : a.processes) - { - std::visit( - [&](const auto& proc) - { - using T = std::decay_t; - semantics::AerosolProcessRef ref; - - if constexpr (std::is_same_v) - { - ref.type = "DISSOLVED_REACTION"; - ref.phases.push_back({ proc.phase, std::nullopt }); - ref.species.push_back({ proc.solvent, std::nullopt }); - for (const auto& r : proc.reactants) - ref.species.push_back({ r.name, std::nullopt }); - for (const auto& p : proc.products) - ref.species.push_back({ p.name, std::nullopt }); - std::vector rep_refs; - for (const auto& [rep_name, _] : proc.rate_constants) - rep_refs.push_back({ rep_name, std::nullopt }); - ref.aerosol_representations = std::move(rep_refs); - } - else if constexpr (std::is_same_v) - { - ref.type = "DISSOLVED_REVERSIBLE_REACTION"; - ref.phases.push_back({ proc.phase, std::nullopt }); - ref.species.push_back({ proc.solvent, std::nullopt }); - for (const auto& r : proc.reactants) - ref.species.push_back({ r.name, std::nullopt }); - for (const auto& p : proc.products) - ref.species.push_back({ p.name, std::nullopt }); - std::vector rep_refs; - for (const auto& [rep_name, _] : proc.forward_rate_constants) - rep_refs.push_back({ rep_name, std::nullopt }); - for (const auto& [rep_name, _] : proc.reverse_rate_constants) - rep_refs.push_back({ rep_name, std::nullopt }); - ref.aerosol_representations = std::move(rep_refs); - } - else if constexpr (std::is_same_v) - { - ref.type = "HENRY_LAW_PHASE_TRANSFER"; - ref.phases.push_back({ proc.gas_phase, std::nullopt }); - ref.phases.push_back({ proc.condensed_phase, std::nullopt }); - ref.species.push_back({ proc.gas_species, std::nullopt }); - ref.species.push_back({ proc.condensed_species, std::nullopt }); - ref.species.push_back({ proc.solvent, std::nullopt }); - } - - input.aerosol_processes.push_back(std::move(ref)); - }, - x); - } - - // ---- Aerosol Constraints -------------------------------------------------------------------------- - for (const auto& x : a.constraints) - { - std::visit( - [&](const auto& con) - { - using T = std::decay_t; - semantics::AerosolProcessRef ref; - - if constexpr (std::is_same_v) - { - ref.type = "HENRY_LAW_EQUILIBRIUM"; - ref.phases.push_back({ con.gas_phase, std::nullopt }); - ref.phases.push_back({ con.condensed_phase, std::nullopt }); - ref.species.push_back({ con.gas_species, std::nullopt }); - ref.species.push_back({ con.condensed_species, std::nullopt }); - ref.species.push_back({ con.solvent, std::nullopt }); - } - else if constexpr (std::is_same_v) - { - ref.type = "DISSOLVED_EQUILIBRIUM"; - ref.phases.push_back({ con.phase, std::nullopt }); - ref.species.push_back({ con.algebraic_species, std::nullopt }); - ref.species.push_back({ con.solvent, std::nullopt }); - for (const auto& r : con.reactants) - ref.species.push_back({ r.name, std::nullopt }); - for (const auto& p : con.products) - ref.species.push_back({ p.name, std::nullopt }); - } - else if constexpr (std::is_same_v) - { - ref.type = "LINEAR_CONSTRAINT"; - ref.phases.push_back({ con.algebraic_phase, std::nullopt }); - ref.species.push_back({ con.algebraic_species, std::nullopt }); - for (const auto& term : con.terms) - { - ref.phases.push_back({ term.phase, std::nullopt }); - ref.species.push_back({ term.name, std::nullopt }); - } - } - - input.aerosol_processes.push_back(std::move(ref)); - }, - x); - } - - Errors errors = ValidateSemantics(input); - Errors aerosol_errors = ValidateAerosol(mechanism); + Errors errors = ValidateGasModel(mechanism); + Errors aerosol_errors = ValidateAerosolModel(mechanism); errors.insert(errors.end(), aerosol_errors.begin(), aerosol_errors.end()); return errors; } From 09287c7bbd8c1e369cf0e926a3e0ec56874c572a Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 18:43:47 -0600 Subject: [PATCH 20/23] construct aerosol instead of constructing representation and processes separtely --- src/detail/v1/aerosol_type_parsers.hpp | 14 ++++--- src/v1/aerosol_type_parsers.cpp | 52 +++++++++++++++----------- src/v1/parser.cpp | 8 +--- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp index 0d455be0..be17d887 100644 --- a/src/detail/v1/aerosol_type_parsers.hpp +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -73,13 +73,15 @@ namespace mechanism_configuration::v1 std::vector ParseAerosolRepresentations(const YAML::Node& objects); - /// @brief Parses the aerosol processes sequence into the processes or constraints + /// @brief Parses the full aerosol section (representations plus the mixed processes/constraints + /// list) into a single Aerosol container. + /// @param species Parsed top-level species, used to source per-species values such as a Henry's-law + /// equilibrium solvent's molecular weight /// @param phases Parsed phases, used to source per-species values such as a phase-transfer's - /// gas-phase diffusion coefficient - void ParseAerosolProcesses( - const YAML::Node& objects, + /// gas-phase diffusion coefficient or a solvent's density + types::Aerosol ParseAerosol( + const YAML::Node& object, const std::vector& species, - const std::vector& phases, - types::Aerosol& aerosol); + const std::vector& phases); } // namespace mechanism_configuration::v1 diff --git a/src/v1/aerosol_type_parsers.cpp b/src/v1/aerosol_type_parsers.cpp index b6ebe35e..589ed655 100644 --- a/src/v1/aerosol_type_parsers.cpp +++ b/src/v1/aerosol_type_parsers.cpp @@ -282,31 +282,41 @@ namespace mechanism_configuration return representations; } - void ParseAerosolProcesses( - const YAML::Node& objects, + types::Aerosol ParseAerosol( + const YAML::Node& object, const std::vector& species, - const std::vector& phases, - types::Aerosol& aerosol) + const std::vector& phases) { - for (const auto& object : objects) - { - const auto type = object[keys::type].as(); + types::Aerosol aerosol; + + if (object[keys::aerosol_representations]) + aerosol.representations = ParseAerosolRepresentations(object[keys::aerosol_representations]); - // Processes - if (type == keys::HenryLawPhaseTransfer_key) - aerosol.processes.emplace_back(ParseHenryLawPhaseTransfer(object, phases)); - else if (type == keys::DissolvedReaction_key) - aerosol.processes.emplace_back(ParseDissolvedReaction(object)); - else if (type == keys::DissolvedReversibleReaction_key) - aerosol.processes.emplace_back(ParseDissolvedReversibleReaction(object)); - // Constraints - else if (type == keys::HenryLawEquilibrium_key) - aerosol.constraints.emplace_back(ParseHenryLawEquilibrium(object, species, phases)); - else if (type == keys::DissolvedEquilibrium_key) - aerosol.constraints.emplace_back(ParseDissolvedEquilibrium(object)); - else if (type == keys::LinearConstraint_key) - aerosol.constraints.emplace_back(ParseLinearConstraint(object)); + if (object[keys::aerosol_processes]) + { + // Aerosol processes section mixes process and constraint entries. + for (const auto& entry : object[keys::aerosol_processes]) + { + const auto type = entry[keys::type].as(); + + // Processes + if (type == keys::HenryLawPhaseTransfer_key) + aerosol.processes.emplace_back(ParseHenryLawPhaseTransfer(entry, phases)); + else if (type == keys::DissolvedReaction_key) + aerosol.processes.emplace_back(ParseDissolvedReaction(entry)); + else if (type == keys::DissolvedReversibleReaction_key) + aerosol.processes.emplace_back(ParseDissolvedReversibleReaction(entry)); + // Constraints + else if (type == keys::HenryLawEquilibrium_key) + aerosol.constraints.emplace_back(ParseHenryLawEquilibrium(entry, species, phases)); + else if (type == keys::DissolvedEquilibrium_key) + aerosol.constraints.emplace_back(ParseDissolvedEquilibrium(entry)); + else if (type == keys::LinearConstraint_key) + aerosol.constraints.emplace_back(ParseLinearConstraint(entry)); + } } + + return aerosol; } } // namespace v1 diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index ba8b615b..5eb9900b 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -424,13 +424,9 @@ namespace mechanism_configuration::v1 { mechanism.reactions = ParseReactions(object[keys::reactions]); } - if (object[keys::aerosol_representations]) + if (object[keys::aerosol_representations] && object[keys::aerosol_processes]) { - mechanism.aerosol.representations = ParseAerosolRepresentations(object[keys::aerosol_representations]); - } - if (object[keys::aerosol_processes]) - { - ParseAerosolProcesses(object[keys::aerosol_processes], mechanism.species, mechanism.phases, mechanism.aerosol); + mechanism.aerosol = ParseAerosol(object, mechanism.species, mechanism.phases); } if (object[keys::name]) { From 170833b4160003d6c78131b5d2c9d2f0effe6ae0 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 19:25:34 -0600 Subject: [PATCH 21/23] code clean up --- src/v1/parser.cpp | 2 + test/unit/test_validate.cpp | 151 ++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/src/v1/parser.cpp b/src/v1/parser.cpp index 5eb9900b..84e8dd30 100644 --- a/src/v1/parser.cpp +++ b/src/v1/parser.cpp @@ -380,6 +380,8 @@ namespace mechanism_configuration::v1 // the structure is clean. Located via BuildSemanticInput so errors carry line:col. if (errors.empty()) { + // Uses the same ValidateSemantics engine as ValidateGasModel, but with + // YAML-derived input so errors include source locations. auto semantic_errors = ValidateSemantics(BuildSemanticInput(object)); AppendFilePath(config_path_, semantic_errors); errors.insert(errors.end(), semantic_errors.begin(), semantic_errors.end()); diff --git a/test/unit/test_validate.cpp b/test/unit/test_validate.cpp index 583913c8..5cef39e2 100644 --- a/test/unit/test_validate.cpp +++ b/test/unit/test_validate.cpp @@ -6,6 +6,8 @@ #include +#include + using namespace mechanism_configuration; namespace @@ -121,3 +123,152 @@ TEST(Validate, DetectsUnknownPhase) EXPECT_TRUE(HasCode(Validate(m), ErrorCode::UnknownPhase)); } + +namespace +{ + // species A (mw), H2O (mw); gas {A: diffusion}, aqueous {A, H2O: density}; + // one UNIFORM_SECTION representation "cloud" over the aqueous phase. + Mechanism AerosolBaseMechanism() + { + Mechanism m; + types::Species a = species("A"); + a.molecular_weight = 0.05; + types::Species h2o = species("H2O"); + h2o.molecular_weight = 0.018; + m.species = { a, h2o }; + + types::Phase gas; + gas.name = "gas"; + types::PhaseSpecies gas_a = phase_species("A"); + gas_a.diffusion_coefficient = 1.5e-5; + gas.species = { gas_a }; + + types::Phase aqueous; + aqueous.name = "aqueous"; + types::PhaseSpecies aqueous_h2o = phase_species("H2O"); + aqueous_h2o.density = 1000.0; + aqueous.species = { phase_species("A"), aqueous_h2o }; + + m.phases = { gas, aqueous }; + + types::UniformSection section; + section.name = "cloud"; + section.phases = { "aqueous" }; + section.min_radius = 1.0e-6; + section.max_radius = 1.0e-5; + m.aerosol.representations = { section }; + + return m; + } + + types::HenryLawPhaseTransfer ValidPhaseTransfer() + { + types::HenryLawPhaseTransfer t; + t.gas_phase = "gas"; + t.gas_species = "A"; + t.condensed_phase = "aqueous"; + t.condensed_species = "A"; + t.solvent = "H2O"; + return t; + } + + types::HenryLawEquilibrium ValidEquilibrium() + { + types::HenryLawEquilibrium e; + e.gas_phase = "gas"; + e.gas_species = "A"; + e.condensed_phase = "aqueous"; + e.condensed_species = "A"; + e.solvent = "H2O"; + return e; + } +} // namespace + +TEST(ValidateAerosol, IgnoresMechanismWithoutAerosolSection) +{ + EXPECT_TRUE(ValidateAerosolModel(BaseMechanism()).empty()); +} + +TEST(ValidateAerosol, AcceptsValidProcessesAndConstraints) +{ + Mechanism m = AerosolBaseMechanism(); + m.aerosol.processes = { ValidPhaseTransfer() }; + + types::DissolvedReaction reaction; + reaction.phase = "aqueous"; + reaction.solvent = "H2O"; + reaction.reactants = { component("A") }; + reaction.products = { component("A") }; + reaction.rate_constants = { { "cloud", types::ArrheniusReferenceTemperature{} } }; // keyed by a declared representation + m.aerosol.processes.push_back(reaction); + + m.aerosol.constraints = { ValidEquilibrium() }; + + EXPECT_TRUE(ValidateAerosolModel(m).empty()); +} + +TEST(ValidateAerosol, DetectsRepresentationReferencingUnknownPhase) +{ + Mechanism m = AerosolBaseMechanism(); + types::SingleMomentMode mode; + mode.name = "mode"; + mode.phases = { "nonexistent" }; // no such phase + mode.geometric_mean_radius = 1.0e-6; + mode.geometric_standard_deviation = 1.6; + m.aerosol.representations.push_back(mode); + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::UnknownPhase)); +} + +TEST(ValidateAerosol, DetectsSpeciesNotRegisteredInCondensedPhase) +{ + Mechanism m = AerosolBaseMechanism(); + types::DissolvedReaction reaction; + reaction.phase = "aqueous"; + reaction.solvent = "H2O"; + reaction.reactants = { component("Q") }; // Q is not registered in the aqueous phase + m.aerosol.processes = { reaction }; + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequestedSpeciesNotRegisteredInPhase)); +} + +TEST(ValidateAerosol, DetectsRateConstantKeyedByUnknownRepresentation) +{ + Mechanism m = AerosolBaseMechanism(); + types::DissolvedReaction reaction; + reaction.phase = "aqueous"; + reaction.solvent = "H2O"; + reaction.reactants = { component("A") }; + reaction.products = { component("A") }; + reaction.rate_constants = { { "not_a_representation", types::ArrheniusReferenceTemperature{} } }; + m.aerosol.processes = { reaction }; + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::UnknownAerosolRepresentation)); +} + +TEST(ValidateAerosol, DetectsMissingGasDiffusionCoefficient) +{ + Mechanism m = AerosolBaseMechanism(); + m.phases[0].species[0].diffusion_coefficient = std::nullopt; // gas-phase A loses its diffusion coefficient + m.aerosol.processes = { ValidPhaseTransfer() }; + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); +} + +TEST(ValidateAerosol, DetectsMissingSolventDensity) +{ + Mechanism m = AerosolBaseMechanism(); + m.phases[1].species[1].density = std::nullopt; // aqueous H2O loses its density + m.aerosol.constraints = { ValidEquilibrium() }; + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); +} + +TEST(ValidateAerosol, DetectsMissingSolventMolecularWeight) +{ + Mechanism m = AerosolBaseMechanism(); + m.species[1].molecular_weight = std::nullopt; // H2O loses its molecular weight + m.aerosol.constraints = { ValidEquilibrium() }; + + EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); +} From 534eabbce4a6caeaa740fd5268ac965cd8067034 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 19:50:02 -0600 Subject: [PATCH 22/23] add unit tests --- src/detail/v1/aerosol_type_parsers.hpp | 1 - test/unit/v1/CMakeLists.txt | 1 + test/unit/v1/test_parse_aerosol.cpp | 96 +++++++++++++++++++ .../equilibrium_missing_solvent_density.json | 35 +++++++ .../phase_transfer_missing_diffusion.json | 36 +++++++ .../aerosol/valid_aerosol.json | 79 +++++++++++++++ 6 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 test/unit/v1/test_parse_aerosol.cpp create mode 100644 test/unit/v1/v1_unit_configs/aerosol/equilibrium_missing_solvent_density.json create mode 100644 test/unit/v1/v1_unit_configs/aerosol/phase_transfer_missing_diffusion.json create mode 100644 test/unit/v1/v1_unit_configs/aerosol/valid_aerosol.json diff --git a/src/detail/v1/aerosol_type_parsers.hpp b/src/detail/v1/aerosol_type_parsers.hpp index be17d887..c49a75fc 100644 --- a/src/detail/v1/aerosol_type_parsers.hpp +++ b/src/detail/v1/aerosol_type_parsers.hpp @@ -13,7 +13,6 @@ #include #include -// Each Parse function assumes its input has already been validated by the matching CheckAerosol*Schema. namespace mechanism_configuration::v1 { // ---------------------------------------- diff --git a/test/unit/v1/CMakeLists.txt b/test/unit/v1/CMakeLists.txt index d33f4188..de8b7dfd 100644 --- a/test/unit/v1/CMakeLists.txt +++ b/test/unit/v1/CMakeLists.txt @@ -3,6 +3,7 @@ create_standard_test(NAME v1_parse_phases SOURCES test_parse_phases.cpp) create_standard_test(NAME v1_parse_species SOURCES test_parse_species.cpp) +create_standard_test(NAME v1_parse_aerosol SOURCES test_parse_aerosol.cpp) ################################################################################ # Copy test data diff --git a/test/unit/v1/test_parse_aerosol.cpp b/test/unit/v1/test_parse_aerosol.cpp new file mode 100644 index 00000000..62d6bf30 --- /dev/null +++ b/test/unit/v1/test_parse_aerosol.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2023–2026 University Corporation for Atmospheric Research +// University of Illinois at Urbana-Champaign +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include + +#include +#include + +using namespace mechanism_configuration; + +namespace +{ + // True if any reported error carries the given code. + bool HasError(const Errors& errors, ErrorCode code) + { + for (const auto& [error_code, message] : errors) + { + std::cout << message << " " << ErrorCodeToString(error_code) << std::endl; + if (error_code == code) + return true; + } + return false; + } +} // namespace + +TEST(ParseAerosol, ParsesValidAerosolConfiguration) +{ + auto parsed = Parse("v1_unit_configs/aerosol/valid_aerosol.json"); + if (!parsed) + for (const auto& [code, message] : parsed.error()) + std::cout << message << std::endl; + ASSERT_TRUE(parsed); + + const Mechanism& mechanism = *parsed; + const auto& aerosol = mechanism.aerosol; + + // Two SINGLE_MOMENT_MODE representations. + ASSERT_EQ(aerosol.representations.size(), 2u); + const auto& aitken = std::get(aerosol.representations[0]); + EXPECT_EQ(aitken.name, "aitken"); + EXPECT_DOUBLE_EQ(aitken.geometric_mean_radius, 1.0e-6); + EXPECT_DOUBLE_EQ(aitken.geometric_standard_deviation, 1.0e-5); + const auto& accumulation = std::get(aerosol.representations[1]); + EXPECT_EQ(accumulation.name, "accumulation"); + EXPECT_DOUBLE_EQ(accumulation.geometric_mean_radius, 2.0e-5); + EXPECT_DOUBLE_EQ(accumulation.geometric_standard_deviation, 1.0e-5); + + // Processes: a phase transfer followed by a dissolved reaction. + ASSERT_EQ(aerosol.processes.size(), 2u); + + // The phase transfer's diffusion coefficient is sourced from the gas-phase species definition. + const auto& transfer = std::get(aerosol.processes[0]); + EXPECT_DOUBLE_EQ(transfer.diffusion_coefficient, 1.5e-5); + EXPECT_DOUBLE_EQ(transfer.accommodation_coefficient, 0.1); + + // The dissolved reaction's components are keyed on "name". + const auto& reaction = std::get(aerosol.processes[1]); + ASSERT_EQ(reaction.reactants.size(), 1u); + ASSERT_EQ(reaction.products.size(), 1u); + EXPECT_EQ(reaction.reactants[0].name, "A"); + EXPECT_EQ(reaction.products[0].name, "B"); + + // Constraints: a Henry's-law equilibrium followed by a linear constraint. + ASSERT_EQ(aerosol.constraints.size(), 2u); + + // The solvent properties are sourced from the species/phase definitions. + const auto& equilibrium = std::get(aerosol.constraints[0]); + EXPECT_EQ(equilibrium.solvent, "H2O"); + EXPECT_DOUBLE_EQ(equilibrium.solvent_molecular_weight, 0.018); // from the species section + EXPECT_DOUBLE_EQ(equilibrium.solvent_density, 1000.0); // from the aqueous phase + + // The linear constraint's terms are keyed on "name". + const auto& linear = std::get(aerosol.constraints[1]); + ASSERT_EQ(linear.terms.size(), 1u); + EXPECT_EQ(linear.terms[0].phase, "gas"); + EXPECT_EQ(linear.terms[0].name, "A"); +} + +TEST(ParseAerosol, RejectsPhaseTransferWhenGasSpeciesHasNoDiffusionCoefficient) +{ + auto parsed = Parse("v1_unit_configs/aerosol/phase_transfer_missing_diffusion.json"); + EXPECT_FALSE(parsed); + ASSERT_FALSE(parsed) << "Expected validation to fail for a missing diffusion coefficient."; + EXPECT_TRUE(HasError(parsed.error(), ErrorCode::RequiredKeyNotFound)); +} + +TEST(ParseAerosol, RejectsHenryLawEquilibriumWhenSolventHasNoDensity) +{ + auto parsed = Parse("v1_unit_configs/aerosol/equilibrium_missing_solvent_density.json"); + EXPECT_FALSE(parsed); + ASSERT_FALSE(parsed) << "Expected validation to fail for a missing solvent density."; + EXPECT_TRUE(HasError(parsed.error(), ErrorCode::RequiredKeyNotFound)); +} diff --git a/test/unit/v1/v1_unit_configs/aerosol/equilibrium_missing_solvent_density.json b/test/unit/v1/v1_unit_configs/aerosol/equilibrium_missing_solvent_density.json new file mode 100644 index 00000000..0520a2ae --- /dev/null +++ b/test/unit/v1/v1_unit_configs/aerosol/equilibrium_missing_solvent_density.json @@ -0,0 +1,35 @@ +{ + "version": "1.2.0", + "name": "equilibrium missing solvent density", + "species": [ + { "name": "A" }, + { "name": "H2O", "molecular weight [kg mol-1]": 0.018 } + ], + "phases": [ + { + "name": "gas", + "species": [ { "name": "A", "diffusion coefficient [m2 s-1]": 1.5e-5 } ] + }, + { "name": "aqueous", "species": [ "A", "H2O" ] } + ], + "aerosol representations": [ + { + "type": "UNIFORM_SECTION", + "name": "cloud", + "phases": ["aqueous"], + "minimum radius [m]": 1.0e-6, + "maximum radius [m]": 1.0e-5 + } + ], + "aerosol processes": [ + { + "type": "HENRY_LAW_EQUILIBRIUM", + "gas phase": "gas", + "gas-phase species": "A", + "condensed phase": "aqueous", + "condensed-phase species": "A", + "solvent": "H2O", + "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.0e-2, "C [K]": 3000.0 } + } + ] +} diff --git a/test/unit/v1/v1_unit_configs/aerosol/phase_transfer_missing_diffusion.json b/test/unit/v1/v1_unit_configs/aerosol/phase_transfer_missing_diffusion.json new file mode 100644 index 00000000..3c8ced66 --- /dev/null +++ b/test/unit/v1/v1_unit_configs/aerosol/phase_transfer_missing_diffusion.json @@ -0,0 +1,36 @@ +{ + "version": "1.2.0", + "name": "phase transfer missing diffusion", + "species": [ + { "name": "A" }, + { "name": "H2O", "molecular weight [kg mol-1]": 0.018 } + ], + "phases": [ + { "name": "gas", "species": ["A"] }, + { + "name": "aqueous", + "species": [ "A", { "name": "H2O", "density [kg m-3]": 1000.0 } ] + } + ], + "aerosol representations": [ + { + "type": "UNIFORM_SECTION", + "name": "cloud", + "phases": ["aqueous"], + "minimum radius [m]": 1.0e-6, + "maximum radius [m]": 1.0e-5 + } + ], + "aerosol processes": [ + { + "type": "HENRY_LAW_PHASE_TRANSFER", + "gas phase": "gas", + "gas-phase species": "A", + "condensed phase": "aqueous", + "condensed-phase species": "A", + "solvent": "H2O", + "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.0e-2, "C [K]": 3000.0 }, + "accommodation coefficient": 0.1 + } + ] +} diff --git a/test/unit/v1/v1_unit_configs/aerosol/valid_aerosol.json b/test/unit/v1/v1_unit_configs/aerosol/valid_aerosol.json new file mode 100644 index 00000000..a1a40566 --- /dev/null +++ b/test/unit/v1/v1_unit_configs/aerosol/valid_aerosol.json @@ -0,0 +1,79 @@ +{ + "version": "1.2.0", + "name": "aerosol unit test", + "species": [ + { "name": "A", "molecular weight [kg mol-1]": 0.05 }, + { "name": "B" }, + { "name": "H2O", "molecular weight [kg mol-1]": 0.018 } + ], + "phases": [ + { + "name": "gas", + "species": [ + { "name": "A", "diffusion coefficient [m2 s-1]": 1.5e-5 } + ] + }, + { + "name": "aqueous", + "species": [ + "A", + "B", + { "name": "H2O", "density [kg m-3]": 1000.0 } + ] + } + ], + "aerosol representations": [ + { + "type": "SINGLE_MOMENT_MODE", + "name": "aitken", + "phases": ["aqueous"], + "geometric mean radius [m]": 1.0e-6, + "geometric standard deviation": 1.0e-5 + }, + { + "type": "SINGLE_MOMENT_MODE", + "name": "accumulation", + "phases": ["aqueous"], + "geometric mean radius [m]": 2.0e-5, + "geometric standard deviation": 1.0e-5 + } + ], + "aerosol processes": [ + { + "type": "HENRY_LAW_PHASE_TRANSFER", + "gas phase": "gas", + "gas-phase species": "A", + "condensed phase": "aqueous", + "condensed-phase species": "A", + "solvent": "H2O", + "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.0e-2, "C [K]": 3000.0 }, + "accommodation coefficient": 0.1 + }, + { + "type": "DISSOLVED_REACTION", + "condensed phase": "aqueous", + "solvent": "H2O", + "reactants": [ { "name": "A", "coefficient": 1 } ], + "products": [ { "name": "B", "coefficient": 1 } ], + "rate constants": { + "aitken": { "type": "ARRHENIUS", "A": 1.0e3, "C": 100.0 }, + "accumulation": { "type": "ARRHENIUS", "A": 1.5e3, "C": 120.0 } } + }, + { + "type": "HENRY_LAW_EQUILIBRIUM", + "gas phase": "gas", + "gas-phase species": "A", + "condensed phase": "aqueous", + "condensed-phase species": "A", + "solvent": "H2O", + "Henry's law constant": { "HLC_ref [mol m-3 Pa-1]": 1.0e-2, "C [K]": 3000.0 } + }, + { + "type": "LINEAR_CONSTRAINT", + "algebraic phase": "gas", + "algebraic species": "A", + "diagnose from state": true, + "terms": [ { "phase": "gas", "name": "A", "coefficient": 1.0 } ] + } + ] +} From 238ca1879e5ca18dbe7191cbd81dd60518053031 Mon Sep 17 00:00:00 2001 From: Jiwon Gim Date: Fri, 26 Jun 2026 20:31:54 -0600 Subject: [PATCH 23/23] cleanup comments --- test/unit/test_validate.cpp | 6 +++--- test/unit/v1/test_parse_aerosol.cpp | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/unit/test_validate.cpp b/test/unit/test_validate.cpp index 5cef39e2..cae4bc09 100644 --- a/test/unit/test_validate.cpp +++ b/test/unit/test_validate.cpp @@ -249,7 +249,7 @@ TEST(ValidateAerosol, DetectsRateConstantKeyedByUnknownRepresentation) TEST(ValidateAerosol, DetectsMissingGasDiffusionCoefficient) { Mechanism m = AerosolBaseMechanism(); - m.phases[0].species[0].diffusion_coefficient = std::nullopt; // gas-phase A loses its diffusion coefficient + m.phases[0].species[0].diffusion_coefficient = std::nullopt; // Remove gas-phase A diffusion coefficient m.aerosol.processes = { ValidPhaseTransfer() }; EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); @@ -258,7 +258,7 @@ TEST(ValidateAerosol, DetectsMissingGasDiffusionCoefficient) TEST(ValidateAerosol, DetectsMissingSolventDensity) { Mechanism m = AerosolBaseMechanism(); - m.phases[1].species[1].density = std::nullopt; // aqueous H2O loses its density + m.phases[1].species[1].density = std::nullopt; // Remove aqueous H2O density m.aerosol.constraints = { ValidEquilibrium() }; EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); @@ -267,7 +267,7 @@ TEST(ValidateAerosol, DetectsMissingSolventDensity) TEST(ValidateAerosol, DetectsMissingSolventMolecularWeight) { Mechanism m = AerosolBaseMechanism(); - m.species[1].molecular_weight = std::nullopt; // H2O loses its molecular weight + m.species[1].molecular_weight = std::nullopt; // Remove H2O molecular weight m.aerosol.constraints = { ValidEquilibrium() }; EXPECT_TRUE(HasCode(ValidateAerosolModel(m), ErrorCode::RequiredKeyNotFound)); diff --git a/test/unit/v1/test_parse_aerosol.cpp b/test/unit/v1/test_parse_aerosol.cpp index 62d6bf30..b448f619 100644 --- a/test/unit/v1/test_parse_aerosol.cpp +++ b/test/unit/v1/test_parse_aerosol.cpp @@ -13,7 +13,6 @@ using namespace mechanism_configuration; namespace { - // True if any reported error carries the given code. bool HasError(const Errors& errors, ErrorCode code) { for (const auto& [error_code, message] : errors)