diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 27f4a17383..5ad8a66cee 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -25,6 +25,36 @@ class any; namespace Cantera { +//! Base class defining common data possessed by both AnyMap and AnyValue +//! objects. +class AnyBase { +public: + AnyBase(); + virtual ~AnyBase() {}; + + //! For values which are derived from an input file, set the line and column + //! of this value in that file. Used for providing context for some error + //! messages. + void setLoc(int line, int column); + + //! Get a value from the metadata applicable to the AnyMap tree containing + //! this node. + const AnyValue& getMetadata(const std::string& key) const; + +protected: + //! Line where this node occurs in the input file + int m_line; + + //! Column where this node occurs in the input file + int m_column; + + //! Metadata relevant to an entire AnyMap tree, such as information about + // the input file used to create it + shared_ptr m_metadata; + + friend class InputFileError; +}; + class AnyMap; //! A wrapper for a variable whose type is determined at runtime @@ -43,7 +73,7 @@ class AnyMap; * - `std::string` * - `std::vector` of any of the above */ -class AnyValue +class AnyValue : public AnyBase { public: AnyValue(); @@ -68,15 +98,6 @@ class AnyValue //! providing informative error messages in class InputFileError. void setKey(const std::string& key); - //! For values which are derived from an input file, set the line and column - //! of this value in that file. Used for providing context for some error - //! messages. - void setLoc(int line, int column); - - //! Get a value from the metadata applicable to the AnyMap tree containing - //! this AnyValue. - const AnyValue& getMetadata(const std::string& key) const; - //! Propagate metadata to any child elements void propagateMetadata(shared_ptr& file); @@ -209,16 +230,6 @@ class AnyValue template void checkSize(const std::vector& v, size_t nMin, size_t nMax) const; - //! Line where this value occurs in the input file - int m_line; - - //! Column where this value occurs in the input file - int m_column; - - //! Metadata relevant to an entire AnyMap tree, such as information about - // the input file used to create it - shared_ptr m_metadata; - //! Key of this value in a parent `AnyMap` std::string m_key; @@ -247,8 +258,6 @@ class AnyValue static bool vector2_eq(const boost::any& lhs, const boost::any& rhs); mutable Comparer m_equals; - - friend class InputFileError; }; //! Implicit conversion to vector @@ -347,7 +356,7 @@ std::vector& AnyValue::asVector(size_t nMin, size_t nMax); * } * ``` */ -class AnyMap +class AnyMap : public AnyBase { public: AnyMap(): m_units() {}; @@ -385,19 +394,10 @@ class AnyMap //! messages std::string keys_str() const; - //! For AnyMaps which are derived from an input file, set the line and - //! column of this AnyMap in that file. Used for providing context for some - //! error messages. - void setLoc(int line, int column); - //! Set a metadata value that applies to this AnyMap and its children. //! Mainly for internal use in reading or writing from files. void setMetadata(const std::string& key, const AnyValue& value); - //! Get a value from the metadata applicable to the AnyMap tree containing - //! this AnyMap. - const AnyValue& getMetadata(const std::string& key) const; - //! Propagate metadata to any child elements void propagateMetadata(shared_ptr& file); @@ -516,23 +516,12 @@ class AnyMap //! The default units that are used to convert stored values UnitSystem m_units; - //! Starting line for this map in the input file - int m_line; - - //! Starting column for this map in the input file - int m_column; - - //! Metadata relevant to an entire AnyMap tree, such as information about - // the input file used to create it - shared_ptr m_metadata; - //! Cache for previously-parsed input (YAML) files. The key is the full path //! to the file, and the second element of the value is the last-modified //! time for the file, which is used to enable change detection. static std::unordered_map> s_cache; friend class AnyValue; - friend class InputFileError; }; // Define begin() and end() to allow use with range-based for loops @@ -553,7 +542,7 @@ class InputFileError : public CanteraError //! `node`. The `message` and `args` are processed as in the CanteraError //! class. template - InputFileError(const std::string& procedure, const AnyValue& node, + InputFileError(const std::string& procedure, const AnyBase& node, const std::string& message, const Args&... args) : CanteraError( procedure, @@ -563,18 +552,21 @@ class InputFileError : public CanteraError } //! Indicate an error occurring in `procedure` while using information from - //! `node`. The `message` and `args` are processed as in the CanteraError - //! class. + //! `node1` and `node2`. The `message` and `args` are processed as in the + //! CanteraError class. template - InputFileError(const std::string& procedure, const AnyMap& node, - const std::string& message, const Args&... args) + InputFileError(const std::string& procedure, const AnyBase& node1, + const AnyBase& node2, const std::string& message, + const Args&... args) : CanteraError( procedure, - formatError(fmt::format(message, args...), - node.m_line, node.m_column, node.m_metadata)) + formatError2(fmt::format(message, args...), + node1.m_line, node1.m_column, node1.m_metadata, + node2.m_line, node2.m_column, node2.m_metadata)) { } + virtual std::string getClass() const { return "InputFileError"; } @@ -582,6 +574,9 @@ class InputFileError : public CanteraError static std::string formatError(const std::string& message, int line, int column, const shared_ptr& metadata); + static std::string formatError2(const std::string& message, + int line1, int column1, const shared_ptr& metadata1, + int line2, int column2, const shared_ptr& metadata2); }; } diff --git a/interfaces/cython/cantera/ck2yaml.py b/interfaces/cython/cantera/ck2yaml.py index 292264f320..5644539569 100644 --- a/interfaces/cython/cantera/ck2yaml.py +++ b/interfaces/cython/cantera/ck2yaml.py @@ -61,6 +61,14 @@ BlockMap = yaml.comments.CommentedMap +logger = logging.getLogger(__name__) +loghandler = logging.StreamHandler(sys.stdout) +logformatter = logging.Formatter('%(message)s') +loghandler.setFormatter(logformatter) +logger.handlers.clear() +logger.addHandler(loghandler) +logger.setLevel(logging.INFO) + def FlowMap(*args, **kwargs): m = yaml.comments.CommentedMap(*args, **kwargs) m.fa.set_flow_style() @@ -757,7 +765,7 @@ def warn(self, message): if self.warning_as_error: raise InputError(message) else: - logging.warning(message) + logger.warning(message) @staticmethod def parse_composition(elements, nElements, width): @@ -1562,7 +1570,7 @@ def readline(): entry = [] if label not in self.species_dict: if skip_undeclared_species: - logging.info('Skipping unexpected species "{0}" while reading thermodynamics entry.'.format(label)) + logger.info('Skipping unexpected species "{0}" while reading thermodynamics entry.'.format(label)) continue else: # Add a new species entry @@ -1612,7 +1620,7 @@ def readline(): except Exception as e: error_line_number = self.line_number - len(current) + 1 error_entry = ''.join(current).rstrip() - logging.info( + logger.info( 'Error while reading thermo entry starting on line {0}:\n' '"""\n{1}\n"""'.format(error_line_number, error_entry) ) @@ -1620,7 +1628,9 @@ def readline(): if label not in self.species_dict: if skip_undeclared_species: - logging.info('Skipping unexpected species "{0}" while reading thermodynamics entry.'.format(label)) + logger.info( + 'Skipping unexpected species "{0}" while' + ' reading thermodynamics entry.'.format(label)) thermo = [] line, comment = readline() current = [] @@ -1740,7 +1750,7 @@ def readline(): reaction, revReaction = self.read_kinetics_entry(kinetics, surface) except Exception as e: self.line_number = line_number - logging.info('Error reading reaction starting on ' + logger.info('Error reading reaction starting on ' 'line {0}:\n"""\n{1}\n"""'.format( line_number, kinetics.rstrip())) raise @@ -1783,51 +1793,12 @@ def readline(): for h in header: self.header_lines.append(h[indent:]) - self.check_duplicate_reactions() - for index, reaction in enumerate(self.reactions): reaction.index = index + 1 if transportLines: self.parse_transport_data(transportLines, path, transport_start_line) - def check_duplicate_reactions(self): - """ - Check for marked (and unmarked!) duplicate reactions. Raise exception - for unmarked duplicate reactions. - - Pressure-independent and pressure-dependent reactions are treated as - different, so they don't need to be marked as duplicate. - """ - possible_duplicates = defaultdict(list) - for r in self.reactions: - k = (tuple(r.reactants), tuple(r.products), r.kinetics.pressure_dependent) - possible_duplicates[k].append(r) - - for reactions in possible_duplicates.values(): - for r1,r2 in itertools.combinations(reactions, 2): - if r1.duplicate and r2.duplicate: - pass # marked duplicate reaction - elif (type(r1.kinetics) == ThreeBody and - type(r2.kinetics) != ThreeBody): - pass - elif (type(r1.kinetics) != ThreeBody and - type(r2.kinetics) == ThreeBody): - pass - elif (r1.third_body.upper() == 'M' and - r1.kinetics.efficiencies.get(r2.third_body) == 0): - pass # explicit zero efficiency - elif (r2.third_body.upper() == 'M' and - r2.kinetics.efficiencies.get(r1.third_body) == 0): - pass # explicit zero efficiency - elif r1.third_body != r2.third_body: - pass # distinct third bodies - else: - raise InputError( - 'Encountered unmarked duplicate reaction {} ' - '(See lines {} and {} of the input file.).', - r1, r1.line_number, r2.line_number) - def parse_transport_data(self, lines, filename, line_offset): """ Parse the Chemkin-format transport data in ``lines`` (a list of strings) @@ -2009,9 +1980,9 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, parser = Parser() if quiet: - logging.basicConfig(level=logging.ERROR) + logger.setLevel(level=logging.ERROR) else: - logging.basicConfig(level=logging.INFO) + logger.setLevel(level=logging.INFO) if permissive is not None: parser.warning_as_error = not permissive @@ -2025,7 +1996,7 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, # Read input mechanism files parser.load_chemkin_file(input_file) except Exception as err: - logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format( + logger.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format( input_file, parser.line_number, err)) raise else: @@ -2040,8 +2011,8 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, parser.load_chemkin_file(thermo_file, skip_undeclared_species=bool(input_file)) except Exception: - logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n".format( - thermo_file, parser.line_number)) + logger.warning("\nERROR: Unable to parse '{0}' near line {1}:\n".format( + thermo_file, parser.line_number)) raise if transport_file: @@ -2067,8 +2038,8 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, # Read input mechanism files parser.load_chemkin_file(surface_file, surface=True) except Exception as err: - logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format( - surface_file, parser.line_number, err)) + logger.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format( + surface_file, parser.line_number, err)) raise if extra_file: @@ -2080,8 +2051,8 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, # Read input mechanism files parser.load_extra_file(extra_file) except Exception as err: - logging.warning("\nERROR: Unable to parse '{0}':\n{1}\n".format( - extra_file, err)) + logger.warning("\nERROR: Unable to parse '{0}':\n{1}\n".format( + extra_file, err)) raise if out_name: @@ -2093,16 +2064,43 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, surface_names = parser.write_yaml(name=phase_name, out_name=out_name) if not quiet: nReactions = len(parser.reactions) + sum(len(surf.reactions) for surf in parser.surfaces) - print('Wrote YAML mechanism file to {0!r}.'.format(out_name)) - print('Mechanism contains {0} species and {1} reactions.'.format(len(parser.species_list), nReactions)) - return surface_names + logger.info('Wrote YAML mechanism file to {0!r}.'.format(out_name)) + logger.info('Mechanism contains {0} species and {1} reactions.'.format( + len(parser.species_list), nReactions)) + return parser, surface_names + + def show_duplicate_reactions(self, error_message): + # Find the reaction numbers of the duplicate reactions by looking at + # the YAML file lines shown in the error message generated by + # Kinetics::checkDuplicates. + reactions = [] + for line in error_message.split('\n'): + match = re.match('>.*# Reaction ([0-9]+)', line) + if match: + reactions.append(int(match.group(1))-1) + + if len(reactions) != 2: + # Something went wrong while parsing the error message, so just + # display it as-is instead of trying to be clever. + logger.warning(error_message) + return + + # Give an error message that references the line numbers in the + # original input file. + equation = str(self.reactions[reactions[0]]) + lines = [self.reactions[i].line_number for i in reactions] + logger.warning('Undeclared duplicate reaction {}\nfound on lines {} and {} of ' + 'the kinetics input file.'.format(equation, lines[0], lines[1])) def convert_mech(input_file, thermo_file=None, transport_file=None, surface_file=None, phase_name='gas', extra_file=None, out_name=None, quiet=False, permissive=None): - return Parser.convert_mech(input_file, thermo_file, transport_file, surface_file, - phase_name, extra_file, out_name, quiet, permissive) + _, surface_names = Parser.convert_mech( + input_file, thermo_file, transport_file, surface_file, phase_name, + extra_file, out_name, quiet, permissive) + return surface_names + def main(argv): @@ -2121,13 +2119,13 @@ def main(argv): repr(' '.join(args))) except getopt.GetoptError as e: - print('ck2yaml.py: Error parsing arguments:') - print(e) - print('Run "ck2yaml.py --help" to see usage help.') + logger.error('ck2yaml.py: Error parsing arguments:') + logger.error(e) + logger.error('Run "ck2yaml.py --help" to see usage help.') sys.exit(1) if not options or '-h' in options or '--help' in options: - print(__doc__) + logger.info(__doc__) sys.exit(0) input_file = options.get('--input') @@ -2139,14 +2137,14 @@ def main(argv): if '--id' in options: phase_name = options.get('--id', 'gas') - logging.warning("\nFutureWarning: " - "Option '--id=...' will be replaced by '--name=...'") + logger.warning("\nFutureWarning: " + "Option '--id=...' will be replaced by '--name=...'") else: phase_name = options.get('--name', 'gas') if not input_file and not thermo_file: - print('At least one of the arguments "--input=..." or "--thermo=..."' - ' must be provided.\nRun "ck2yaml.py --help" to see usage help.') + logger.error('At least one of the arguments "--input=..." or "--thermo=..."' + ' must be provided.\nRun "ck2yaml.py --help" to see usage help.') sys.exit(1) extra_file = options.get('--extra') @@ -2160,9 +2158,9 @@ def main(argv): else: out_name = os.path.splitext(thermo_file)[0] + '.yaml' - surfaces = Parser.convert_mech(input_file, thermo_file, transport_file, - surface_file, phase_name, extra_file, - out_name, quiet, permissive) + parser, surfaces = Parser.convert_mech(input_file, thermo_file, + transport_file, surface_file, phase_name, extra_file, out_name, + quiet, permissive) # Do full validation by importing the resulting mechanism if not input_file: @@ -2175,19 +2173,23 @@ def main(argv): try: import cantera as ct except ImportError: - print('WARNING: Unable to import Cantera Python module. Output ' - 'mechanism has not been validated') + logger.warning('WARNING: Unable to import Cantera Python module. ' + 'Output mechanism has not been validated') sys.exit(0) try: - print('Validating mechanism...', end='') + logger.info('Validating mechanism...') gas = ct.Solution(out_name) for surf_name in surfaces: phase = ct.Interface(out_name, surf_name, [gas]) - print('PASSED.') + logger.info('PASSED') except RuntimeError as e: - print('FAILED.') - print(e) + logger.info('FAILED') + msg = str(e) + if 'Undeclared duplicate reactions' in msg: + parser.show_duplicate_reactions(msg) + else: + logger.warning(e) sys.exit(1) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index d94e11c93c..c31ef2cb0c 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -2,6 +2,9 @@ from os.path import join as pjoin import itertools from pathlib import Path +import logging +import io +import sys from . import utilities import cantera as ct @@ -475,6 +478,36 @@ def test_extra(self): for key in ['foo', 'bar']: self.assertIn(key, yml.keys()) + def test_duplicate_reactions(self): + # Running a test this way instead of using the convertMech function + # tests the behavior of the ck2yaml.main function and the mechanism + # validation step. + + # clear global handler created by logging.basicConfig() in ck2cti + logging.getLogger().handlers.clear() + + # Replace the ck2yaml logger with our own in order to capture the output + log_stream = io.StringIO() + logger = logging.getLogger('cantera.ck2yaml') + original_handler = logger.handlers.pop() + logformatter = logging.Formatter('%(message)s') + handler = logging.StreamHandler(log_stream) + handler.setFormatter(logformatter) + logger.addHandler(handler) + + with self.assertRaises(SystemExit): + ck2yaml.main([ + '--input={}/undeclared-duplicate-reactions.inp'.format(self.test_data_dir), + '--thermo={}/dummy-thermo.dat'.format(self.test_data_dir)]) + + # Put the original logger back in place + logger.handlers.clear() + logger.addHandler(original_handler) + + message = log_stream.getvalue() + for token in ('FAILED', 'lines 12 and 14', 'R1A', 'R1B'): + self.assertIn(token, message) + class CtmlConverterTest(utilities.CanteraTest): def test_sofc(self): diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index b070038ed3..8dee70a8ec 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -265,12 +265,32 @@ std::map AnyValue::s_typenames = { std::unordered_map> AnyMap::s_cache; +// Methods of class AnyBase + +AnyBase::AnyBase() + : m_line(-1) + , m_column(-1) +{} + +void AnyBase::setLoc(int line, int column) +{ + m_line = line; + m_column = column; +} + +const AnyValue& AnyBase::getMetadata(const std::string& key) const +{ + if (m_metadata && m_metadata->hasKey(key)) { + return m_metadata->at(key); + } else { + return Empty; + } +} + // Methods of class AnyValue AnyValue::AnyValue() - : m_line(-1) - , m_column(-1) - , m_key() + : m_key() , m_value(new boost::any{}) , m_equals(eq_comparer) {} @@ -278,9 +298,7 @@ AnyValue::AnyValue() AnyValue::~AnyValue() = default; AnyValue::AnyValue(AnyValue const& other) - : m_line(other.m_line) - , m_column(other.m_column) - , m_metadata(other.m_metadata) + : AnyBase(other) , m_key(other.m_key) , m_value(new boost::any{*other.m_value}) , m_equals(other.m_equals) @@ -288,9 +306,7 @@ AnyValue::AnyValue(AnyValue const& other) } AnyValue::AnyValue(AnyValue&& other) - : m_line(other.m_line) - , m_column(other.m_column) - , m_metadata(std::move(other.m_metadata)) + : AnyBase(std::move(other)) , m_key(std::move(other.m_key)) , m_value(std::move(other.m_value)) , m_equals(std::move(other.m_equals)) @@ -298,11 +314,10 @@ AnyValue::AnyValue(AnyValue&& other) } AnyValue& AnyValue::operator=(AnyValue const& other) { - if (this == &other) + if (this == &other) { return *this; - m_line = other.m_line; - m_column = other.m_column; - m_metadata = other.m_metadata; + } + AnyBase::operator=(*this); m_key = other.m_key; m_value.reset(new boost::any{*other.m_value}); m_equals = other.m_equals; @@ -310,11 +325,10 @@ AnyValue& AnyValue::operator=(AnyValue const& other) { } AnyValue& AnyValue::operator=(AnyValue&& other) { - if (this == &other) + if (this == &other) { return *this; - m_line = other.m_line; - m_column = other.m_column; - m_metadata = std::move(other.m_metadata); + } + AnyBase::operator=(std::move(other)); m_key = std::move(other.m_key); m_value = std::move(other.m_value); m_equals = std::move(other.m_equals); @@ -351,21 +365,6 @@ const std::type_info &AnyValue::type() const { return m_value->type(); } -void AnyValue::setLoc(int line, int column) -{ - m_line = line; - m_column = column; -} - -const AnyValue& AnyValue::getMetadata(const std::string& key) const -{ - if (m_metadata && m_metadata->hasKey(key)) { - return m_metadata->at(key); - } else { - return Empty; - } -} - void AnyValue::propagateMetadata(shared_ptr& metadata) { m_metadata = metadata; @@ -998,21 +997,6 @@ std::string AnyMap::keys_str() const return to_string(b); } -void AnyMap::setLoc(int line, int column) -{ - m_line = line; - m_column = column; -} - -const AnyValue& AnyMap::getMetadata(const std::string& key) const -{ - if (m_metadata && m_metadata->hasKey(key)) { - return m_metadata->at(key); - } else { - return Empty; - } -} - void AnyMap::propagateMetadata(shared_ptr& metadata) { m_metadata = metadata; @@ -1222,23 +1206,15 @@ AnyMap::Iterator end(const AnyValue& v) { return v.as().end(); } -std::string InputFileError::formatError(const std::string& message, - int lineno, int column, - const shared_ptr& metadata) +namespace { +void formatInputFile(fmt::memory_buffer& b, const shared_ptr& metadata, + const std::string& filename, int lineno, int column, int lineno2=-1, int column2=-1) { - if (!metadata) { - return message; + if (lineno2 == -1) { + lineno2 = lineno; + column2 = column; } - std::string filename = metadata->getString("filename", ""); - fmt::memory_buffer b; - format_to(b, "Error on line {} of", lineno+1); - if (filename.empty()) { - format_to(b, " input string:\n"); - } else { - format_to(b, " {}:\n", filename); - } - format_to(b, "{}\n", message); format_to(b, "| Line |\n"); if (!metadata->hasKey("file-contents")) { std::ifstream infile(findInputFile(filename)); @@ -1248,16 +1224,68 @@ std::string InputFileError::formatError(const std::string& message, } std::string line; int i = 0; + int lastShown = -1; std::stringstream contents((*metadata)["file-contents"].asString()); while (std::getline(contents, line)) { - if (lineno == i) { + if (i == lineno || i == lineno2) { format_to(b, "> {: 5d} > {}\n", i+1, line); format_to(b, "{:>{}}\n", "^", column + 11); - } else if (lineno + 4 > i && lineno < i + 6) { + lastShown = i; + } else if ((lineno + 4 > i && lineno < i + 6) || + (lineno2 + 4 > i && lineno2 < i + 6)) { + if (lastShown >= 0 && i - lastShown > 1) { + format_to(b, "...\n"); + } format_to(b, "| {: 5d} | {}\n", i+1, line); + lastShown = i; } i++; } +} +} + +std::string InputFileError::formatError(const std::string& message, + int lineno, int column, + const shared_ptr& metadata) +{ + if (!metadata) { + return message; + } + std::string filename = metadata->getString("filename", "input string"); + + fmt::memory_buffer b; + format_to(b, "Error on line {} of {}:\n{}\n", lineno+1, filename, message); + formatInputFile(b, metadata, filename, lineno, column); + return to_string(b); +} + +std::string InputFileError::formatError2(const std::string& message, + int line1, int column1, + const shared_ptr& metadata1, + int line2, int column2, + const shared_ptr& metadata2) +{ + if (!metadata1 || !metadata2) { + return message; + } + std::string filename1 = metadata1->getString("filename", "input string"); + std::string filename2 = metadata2->getString("filename", "input string"); + + fmt::memory_buffer b; + if (filename1 == filename2) { + format_to(b, "Error on lines {} and {} of {}:\n", + std::min(line1, line2) + 1, std::max(line1, line2) + 1, + filename1); + format_to(b, "{}\n", message); + formatInputFile(b, metadata1, filename1, line1, column1, line2, column2); + } else { + format_to(b, "Error on line {} of {} and line {} of {}:\n{}\n", + line1+1, filename1, line2+1, filename2, message); + formatInputFile(b, metadata1, filename1, line1, column1); + format_to(b, "\n"); + formatInputFile(b, metadata2, filename2, line2, column2); + } + return to_string(b); } diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index caf77be85e..a9f04c7d12 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -156,7 +156,8 @@ std::pair Kinetics::checkDuplicates(bool throw_err) const } } if (throw_err) { - throw CanteraError("Kinetics::checkDuplicates", + throw InputFileError("Kinetics::checkDuplicates", + R.input, other.input, "Undeclared duplicate reactions detected:\n" "Reaction {}: {}\nReaction {}: {}\n", i+1, other.equation(), m+1, R.equation()); @@ -169,7 +170,8 @@ std::pair Kinetics::checkDuplicates(bool throw_err) const if (unmatched_duplicates.size()) { size_t i = *unmatched_duplicates.begin(); if (throw_err) { - throw CanteraError("Kinetics::checkDuplicates", + throw InputFileError("Kinetics::checkDuplicates", + m_reactions[i]->input, "No duplicate found for declared duplicate reaction number {}" " ({})", i, m_reactions[i]->equation()); } else { @@ -258,9 +260,10 @@ void Kinetics::checkReactionBalance(const Reaction& R) } } if (!ok) { - msg = "The following reaction is unbalanced: " + R.equation() + "\n" + - " Element Reactants Products\n" + msg; - throw CanteraError("Kinetics::checkReactionBalance", msg); + throw InputFileError("Kinetics::checkReactionBalance", R.input, + "The following reaction is unbalanced: {}\n" + " Element Reactants Products\n{}", + R.equation(), msg); } } @@ -483,8 +486,8 @@ bool Kinetics::addReaction(shared_ptr r) // is not reversible, since computing the reverse rate from thermochemistry // only works for elementary reactions. if (r->reversible && !r->orders.empty()) { - throw CanteraError("Kinetics::addReaction", "Reaction orders may only " - "be given for irreversible reactions"); + throw InputFileError("Kinetics::addReaction", r->input, + "Reaction orders may only be given for irreversible reactions"); } // Check for undeclared species @@ -493,9 +496,9 @@ bool Kinetics::addReaction(shared_ptr r) if (m_skipUndeclaredSpecies) { return false; } else { - throw CanteraError("Kinetics::addReaction", "Reaction '" + - r->equation() + "' contains the undeclared species '" + - sp.first + "'"); + throw InputFileError("Kinetics::addReaction", r->input, + "Reaction '{}' contains the undeclared species '{}'", + r->equation(), sp.first); } } } @@ -504,9 +507,9 @@ bool Kinetics::addReaction(shared_ptr r) if (m_skipUndeclaredSpecies) { return false; } else { - throw CanteraError("Kinetics::addReaction", "Reaction '" + - r->equation() + "' contains the undeclared species '" + - sp.first + "'"); + throw InputFileError("Kinetics::addReaction", r->input, + "Reaction '{}' contains the undeclared species '{}'", + r->equation(), sp.first); } } } @@ -515,8 +518,9 @@ bool Kinetics::addReaction(shared_ptr r) if (m_skipUndeclaredSpecies) { return false; } else { - throw CanteraError("Kinetics::addReaction", "Reaction '{}' has " - "a reaction order specified for the undeclared species '{}'", + throw InputFileError("Kinetics::addReaction", r->input, + "Reaction '{}' has a reaction order specified for the " + "undeclared species '{}'", r->equation(), sp.first); } } diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index effed04a9d..998c9b10b5 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -177,6 +177,8 @@ void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode } } } + + kin.checkDuplicates(); } } diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index ff40c20ea0..1832095905 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -429,7 +429,7 @@ void addSpecies(ThermoPhase& thermo, const AnyValue& names, const AnyValue& spec if (species_nodes.count(name)) { thermo.addSpecies(newSpecies(*species_nodes.at(name))); } else { - throw InputFileError("addSpecies", species, + throw InputFileError("addSpecies", names, species, "Could not find a species named '{}'.", name); } } diff --git a/test/data/undeclared-duplicate-reactions.inp b/test/data/undeclared-duplicate-reactions.inp new file mode 100644 index 0000000000..80ffe18821 --- /dev/null +++ b/test/data/undeclared-duplicate-reactions.inp @@ -0,0 +1,15 @@ +ELEMENTS +H C +END + +SPECIES +H +R1A R1B P1 R3 P3A P3B +END + +REACTIONS +R3+H <=> P3A+P3B 1.0e19 0.0 5000.0 +R1A+R1B <=> P1+H 1.0e19 0.0 5000.0 +R1A+R1B+M <=> P1+H+M 1.0e-2 0.0 5000.0 +P1+H => R1A+R1B 1.0e19 0.0 5000.0 +END