diff --git a/documentation/source/users/rmg/input.rst b/documentation/source/users/rmg/input.rst index 40663fc8d4b..17635f33b56 100644 --- a/documentation/source/users/rmg/input.rst +++ b/documentation/source/users/rmg/input.rst @@ -1073,13 +1073,17 @@ Miscellaneous options:: units='si', generateOutputHTML=True, generatePlots=False, - generatePESDiagrams=False, + generatePESDiagrams=False, saveSimulationProfiles=True, verboseComments=False, saveEdgeSpecies=True, keepIrreversible=True, trimolecularProductReversible=False, - saveSeedModulus=-1 + saveSeedModulus=-1, + generateChemkin=True, + generateRMSYAML=True, + generateCanteraYAML1=False, + generateCanteraYAML2=False, ) The ``name`` field is the name of any generated seed mechanisms @@ -1099,9 +1103,9 @@ Setting ``generatePESDiagrams`` to ``True`` will generate potential energy surfa Setting ``saveSimulationProfiles`` to ``True`` will make RMG save csv files of the simulation in .csv files in the ``solver/`` folder. The filename will be ``simulation_1_26.csv`` where the first number corresponds to the reaciton system, and the second number corresponds to the total number of species at the point of the simulation. Therefore, the highest second number will indicate the latest simulation that RMG has complete while enlarging the core model. The information inside the csv file will provide the time, reactor volume in m^3, as well as mole fractions of the individual species. -Setting ``verboseComments`` to ``True`` will make RMG generate chemkin files with complete verbose commentary for the kinetic and thermo parameters. This will be helpful in debugging what values are being averaged for the kinetics. Note that this may produce very large files. +Setting ``verboseComments`` to ``True`` will make RMG generate chemkin files with complete verbose commentary for the kinetic and thermo parameters. This will be helpful in debugging what values are being averaged for the kinetics. Note that this may produce very large files. This is a global fallback; individual writers can override it (see below). -Setting ``saveEdgeSpecies`` to ``True`` will make RMG generate chemkin files of the edge reactions in addition to the core model in files such as ``chem_edge.inp`` and ``chem_edge_annotated.inp`` files located inside the ``chemkin`` folder. These files will be helpful in viewing RMG's estimate for edge reactions and seeing if certain reactions one expects are actually in the edge or not. +Setting ``saveEdgeSpecies`` to ``True`` will make RMG generate chemkin files of the edge reactions in addition to the core model in files such as ``chem_edge.inp`` and ``chem_edge_annotated.inp`` files located inside the ``chemkin`` folder. These files will be helpful in viewing RMG's estimate for edge reactions and seeing if certain reactions one expects are actually in the edge or not. This is a global fallback; individual writers can override it (see below). Setting ``keepIrreversible`` to ``True`` will make RMG import library reactions as is, whether they are reversible or irreversible in the library. Otherwise, if ``False`` (default value), RMG will force all library reactions to be reversible, and will assign the forward rate from the relevant library. @@ -1109,6 +1113,49 @@ Setting ``trimolecularProductReversible`` to ``False`` will not allow families w Setting ``saveSeedModulus`` to ``-1`` will only save the seed from the last iteration at the end of an RMG job. Alternatively, the seed can be saved every ``n`` iterations by setting ``saveSeedModulus`` to ``n``. +Per-writer Output Configuration +-------------------------------- + +Each of the following options controls a separate output-format writer. Each +accepts ``True``, ``False``, or a Python dict with optional keys: + +* ``'saveInterval'`` *(int)* — positive N writes every N iterations (iteration + numbering starts at 0); ``-1`` writes only at the very end of the run. + Defaults to ``1`` (every iteration) for writers that are on by default. +* ``'verboseComments'`` *(bool, optional)* — overrides the global + ``verboseComments`` flag for this writer only. +* ``'saveEdge'`` *(bool, optional)* — overrides the global ``saveEdgeSpecies`` + flag for this writer only. + +Examples:: + + # Chemkin: save only at the end, with verbose comments and edge species + generateChemkin={'saveInterval': -1, 'verboseComments': True, 'saveEdge': True} + + # RMS YAML: save every 5 iterations + generateRMSYAML={'saveInterval': 5} + + # Cantera YAML v2: save every iteration with verbose comments + generateCanteraYAML2={'saveInterval': 1, 'verboseComments': True, 'saveEdge': False} + +``generateChemkin`` (default ``True``) + Controls the Chemkin writer. Output is written to the ``chemkin/`` folder. + +``generateRMSYAML`` (default ``True``) + Controls the RMS YAML writer. Output is written to the ``rms/`` folder. + +``generateCanteraYAML1`` (default ``False``) + Controls the Cantera YAML v1 writer. Output is written to the ``cantera1/`` + folder. This writer is disabled by default. + +``generateCanteraYAML2`` (default ``False``) + Controls the Cantera YAML v2 writer. Output is written to the ``cantera2/`` + folder. This writer is disabled by default. + +``generateOutputHTML`` (default ``False``) + Controls the HTML species-visualisation writer. Output is written to the + ``species/`` folder. Accepts ``True``/``False`` or the same dict format. + Species Constraints ===================== diff --git a/documentation/source/users/rmg/running.rst b/documentation/source/users/rmg/running.rst index 0755c6199e7..43425fe38b4 100755 --- a/documentation/source/users/rmg/running.rst +++ b/documentation/source/users/rmg/running.rst @@ -48,9 +48,9 @@ at the command line will print the documentation from ``util.py``, which is repr -P, --postprocess postprocess profiling statistics from previous [failed] run; does not run the simulation -t DD:HH:MM:SS, --walltime DD:HH:MM:SS - set the maximum execution time + set the maximum execution time (overrides input.py if provided) -i MAXITER, --maxiter MAXITER - set the maximum number of RMG iterations + set the maximum number of RMG iterations (overrides input.py if provided) -n MAXPROC, --maxproc MAXPROC max number of processes used during reaction generation @@ -73,7 +73,7 @@ Run with multiprocessing for reaction generation and QMTP:: python rmg.py -n input.py -Run with setting a limit on the maximum execution time:: +Run with setting a limit on the maximum execution time (if specified, then the command-line value overrides any value read from ``input.py``):: python rmg.py -t input.py diff --git a/examples/rmg/1,3-hexadiene/input.py b/examples/rmg/1,3-hexadiene/input.py index f3706af0f7d..3e03b201c66 100644 --- a/examples/rmg/1,3-hexadiene/input.py +++ b/examples/rmg/1,3-hexadiene/input.py @@ -90,4 +90,9 @@ generateOutputHTML=False, generatePlots=False, generatePESDiagrams=True, + # Large model: write output every 5 iterations and skip edge species to reduce I/O + generateChemkin={'saveInterval': 1, 'saveEdge': False}, + generateRMSYAML={'saveInterval': 5}, + generateCanteraYAML1={'saveInterval': 5}, + generateCanteraYAML2={'saveInterval': 5}, ) diff --git a/examples/rmg/MR_test/input.py b/examples/rmg/MR_test/input.py index 956c7258379..bea097546a1 100644 --- a/examples/rmg/MR_test/input.py +++ b/examples/rmg/MR_test/input.py @@ -260,9 +260,9 @@ #Sets a time limit in the form DD:HH:MM:SS after which the RMG job will stop. Useful for profiling on jobs that #do not converge. #wallTime = '00:00:00', + #When keepIrreversible=False (default), forces RMG to import library reactions as reversible. + #Otherwise, if set to True, RMG will import library reactions while keeping the reversibility as specified. keepIrreversible=False, - #Forces RMG to import library reactions as reversible (default). Otherwise, if set to True, RMG will import library - #reactions while keeping the reversibility as as. ) # optional module allows for correction to unimolecular reaction rates at low pressures and/or temperatures. diff --git a/examples/rmg/ch3no2/input.py b/examples/rmg/ch3no2/input.py index 1e1de348eab..5cebce3dbdc 100644 --- a/examples/rmg/ch3no2/input.py +++ b/examples/rmg/ch3no2/input.py @@ -80,8 +80,13 @@ ) simulator(atol=1e-16,rtol=1e-8) + options( units='si', generateOutputHTML=False, generatePlots=False, + # Large model: write output every 5 iterations to reduce I/O + generateChemkin={'saveInterval': 5, 'saveEdge': False}, + generateRMSYAML={'saveInterval': 5}, + generateCanteraYAML2={'saveInterval': 10, 'saveEdge': True}, ) diff --git a/examples/rmg/commented/input.py b/examples/rmg/commented/input.py index 2c74b3700be..95c2c8eecc2 100644 --- a/examples/rmg/commented/input.py +++ b/examples/rmg/commented/input.py @@ -221,23 +221,47 @@ generatePESDiagrams=False, # saves mole fraction of species in 'solver/' to help you create plots saveSimulationProfiles=False, - # gets RMG to output comments on where kinetics were obtained in the chemkin file. - # useful for debugging kinetics but increases memory usage of the chemkin output file + # Global fallback for verbose comments (comments on where kinetics were obtained). + # Useful for debugging kinetics but increases output file size. + # Individual writers can override this with their own verboseComments key. verboseComments=False, - # gets RMG to generate edge species chemkin files. Uses lots of memory in output. - # Helpful for seeing why some reaction are not appearing in core model. + # Global fallback for saving edge-species files. Uses lots of memory in output. + # Helpful for seeing why some reactions are not appearing in the core model. + # Individual writers can override this with their own saveEdge key. saveEdgeSpecies=False, - # Sets a time limit in the form DD:HH:MM:SS after which the RMG job will stop. Useful for profiling on jobs that - # do not converge. - # wallTime = '00:00:00', - # Forces RMG to import library reactions as reversible (default). Otherwise, if set to True, RMG will import library - # reactions while keeping the reversibility as as. + # Sets a time limit in the form DD:HH:MM:SS after (or shortly before) which the RMG job will stop. + # Useful for profiling on jobs that do not converge. + wallTime = '00:00:00:00', + # If keepIrreversible=False (default) forces RMG to import library reactions as reversible. + # If set to True, RMG will import library reactions while keeping the reversibility as specified. keepIrreversible=False, # Allows families with three products to react in the diverse direction (default). trimolecularProductReversible=True, # Allows a seed to be saved every n iterations. # The default of -1 causes the iteration to only be saved at the end of the RMG job - saveSeedModulus=-1 + saveSeedModulus=-1, + # + # --- Per-writer output configuration --- + # Each writer accepts True/False or a dict with keys: + # 'saveInterval': N (positive = every N iterations; -1 = end of run only) + # 'verboseComments': True/False (overrides the global verboseComments above) + # 'saveEdge': True/False (overrides the global saveEdgeSpecies above) + # + # Chemkin writer: always on by default; saves every iteration. + generateChemkin=True, + # generateChemkin={'saveInterval': -1, 'verboseComments': True, 'saveEdge': True}, + # + # RMS YAML writer: always on by default; saves every iteration. + generateRMSYAML=True, + # generateRMSYAML={'saveInterval': -1}, + # + # Cantera YAML v1 writer: off by default. + generateCanteraYAML1=False, + # generateCanteraYAML1={'saveInterval': -1, 'verboseComments': True, 'saveEdge': False}, + # + # Cantera YAML v2 writer: off by default. + generateCanteraYAML2=False, + # generateCanteraYAML2={'saveInterval': 1, 'verboseComments': True, 'saveEdge': True}, ) # optional module allows for correction to unimolecular reaction rates at low pressures and/or temperatures. diff --git a/examples/rmg/diesel/input.py b/examples/rmg/diesel/input.py index 0492c2b8db2..4fae7410917 100644 --- a/examples/rmg/diesel/input.py +++ b/examples/rmg/diesel/input.py @@ -86,4 +86,9 @@ units='si', generateOutputHTML=False, generatePlots=False, + # Large model: write output every 5 iterations and skip edge species to reduce I/O + generateChemkin={'saveInterval': 5, 'saveEdge': False}, + generateRMSYAML={'saveInterval': 5}, + generateCanteraYAML1={'saveInterval': 10}, + generateCanteraYAML2={'saveInterval': 10}, ) diff --git a/examples/rmg/e85/input.py b/examples/rmg/e85/input.py index 66529b9280c..2b895c27374 100644 --- a/examples/rmg/e85/input.py +++ b/examples/rmg/e85/input.py @@ -87,5 +87,9 @@ units='si', generateOutputHTML=False, generatePlots=False, + generateChemkin={'saveInterval': 5, 'saveEdge': True}, + generateRMSYAML={'saveInterval': 5}, + generateCanteraYAML1={'saveInterval': 5}, + generateCanteraYAML2={'saveInterval': 5}, ) diff --git a/examples/rmg/heptane-eg5/input.py b/examples/rmg/heptane-eg5/input.py index ee19001b306..b3d9b50e775 100644 --- a/examples/rmg/heptane-eg5/input.py +++ b/examples/rmg/heptane-eg5/input.py @@ -3,7 +3,7 @@ thermoLibraries = ['primaryThermoLibrary'], reactionLibraries = [], seedMechanisms = [], - kineticsDepositories = ['training'], + kineticsDepositories = ['training'], kineticsFamilies = 'default', kineticsEstimator = 'rate rules', ) @@ -71,4 +71,12 @@ interpolation=('Chebyshev', 6, 4), ) - +options( + units='si', + generateOutputHTML={'saveInterval': 10, 'saveEdge': True}, + generatePlots=False, + generateChemkin={'saveInterval': 1, 'saveEdge': False}, + generateRMSYAML={'saveInterval': 5}, + generateCanteraYAML1={'saveInterval': 5, 'saveEdge': True}, + generateCanteraYAML2={'saveInterval': 10, 'saveEdge': True}, +) diff --git a/rmgpy/__main__.py b/rmgpy/__main__.py index 6127d761f80..5e85b6c99bf 100644 --- a/rmgpy/__main__.py +++ b/rmgpy/__main__.py @@ -62,11 +62,13 @@ def main(): kwargs = { 'restart': args.restart, - 'walltime': args.walltime, 'maxproc': args.maxproc, 'kineticsdatastore': args.kineticsdatastore, - 'max_iterations': args.maxiter, } + if args.walltime is not None: + kwargs['walltime'] = args.walltime + if args.maxiter is not None: + kwargs['max_iterations'] = args.maxiter if args.profile: import cProfile diff --git a/rmgpy/chemkin.pyx b/rmgpy/chemkin.pyx index 636cbe09264..eaf3a96f22c 100644 --- a/rmgpy/chemkin.pyx +++ b/rmgpy/chemkin.pyx @@ -2272,10 +2272,12 @@ def save_chemkin(reaction_model, path, verbose_path, dictionary_path=None, trans save_transport_file(transport_path, species_list) -def save_chemkin_files(rmg): +def save_chemkin_files(rmg, config=None): """ Save the current reaction model to a set of Chemkin files. """ + verbose = config.verbose_comments if (config and config.verbose_comments is not None) else rmg.verbose_comments + save_edge = config.save_edge if (config and config.save_edge is not None) else rmg.save_edge_species # todo: make this an attribute or method of reactionModel is_surface_model = any([s.contains_surface_site() for s in rmg.reaction_model.core.species]) @@ -2310,7 +2312,7 @@ def save_chemkin_files(rmg): os.unlink(latest_chemkin_path) shutil.copy2(this_chemkin_path, latest_chemkin_path) - if rmg.save_edge_species: + if save_edge: logging.info('Saving current model core and edge to Chemkin file...') this_chemkin_path = os.path.join(rmg.output_directory, 'chemkin', 'chem_edge{0:04d}.inp'.format(len(rmg.reaction_model.core.species))) @@ -2319,7 +2321,7 @@ def save_chemkin_files(rmg): latest_dictionary_path = os.path.join(rmg.output_directory, 'chemkin', 'species_edge_dictionary.txt') latest_transport_path = None save_chemkin(rmg.reaction_model, this_chemkin_path, latest_chemkin_verbose_path, latest_dictionary_path, - latest_transport_path, rmg.save_edge_species) + latest_transport_path, save_edge) if is_surface_model: paths = [] @@ -2386,9 +2388,13 @@ class ChemkinWriter(object): rmg.detach(listener) """ - def __init__(self, output_directory=''): + def __init__(self, output_directory='', config=None): super(ChemkinWriter, self).__init__() + self.config = config make_output_subdirectory(output_directory, 'chemkin') def update(self, rmg): - save_chemkin_files(rmg) + if self.config is not None and not self.config.should_write( + rmg.reaction_model.iteration_num, rmg.is_final_save): + return + save_chemkin_files(rmg, config=self.config) diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index 887c0f9eab8..97399c0dd03 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -53,7 +53,7 @@ ConstantVIdealGasReactor, Reactor, ) -from rmgpy.rmg.settings import ModelSettings, SimulatorSettings +from rmgpy.rmg.settings import ModelSettings, SimulatorSettings, WriterConfig from rmgpy.solver.liquid import LiquidReactor from rmgpy.solver.mbSampled import MBSampledReactor from rmgpy.solver.simple import SimpleReactor @@ -1418,10 +1418,65 @@ def pressure_dependence( rmg.reaction_model.add_completed_pdep_network(formula) +def _parse_writer_config(value, default_save_interval=1): + """ + Parse an output-writer configuration value from an input file. + + Parameters + ---------- + value : bool or dict + ``False`` disables the writer. ``True`` enables it with + *default_save_interval*. A dict may contain the keys + ``'saveInterval'`` (int), ``'verboseComments'`` (bool), and + ``'saveEdge'`` (bool). + default_save_interval : int + Save interval to use when ``value`` is ``True``. + + Returns + ------- + WriterConfig + """ + if value is False: + return WriterConfig(save_interval=0) + if value is True: + return WriterConfig(save_interval=default_save_interval) + if isinstance(value, dict): + si = value.get('saveInterval', default_save_interval) + return WriterConfig( + save_interval=si, + verbose_comments=value.get('verboseComments', None), + save_edge=value.get('saveEdge', None), + ) + raise InputError( + f"Writer config must be True, False, or a dict with keys " + f"'saveInterval', 'verboseComments', 'saveEdge'; got {type(value).__name__!r}" + ) + + +def _writer_config_to_input(cfg): + """ + Serialize a WriterConfig back to a value suitable for writing into an + RMG input file (i.e. ``True``, ``False``, or a dict literal string). + """ + if cfg is None or not cfg.enabled: + return False + has_overrides = (cfg.verbose_comments is not None or cfg.save_edge is not None) + if cfg.save_interval == 1 and not has_overrides: + return True + parts = [f"'saveInterval': {cfg.save_interval}"] + if cfg.verbose_comments is not None: + parts.append(f"'verboseComments': {cfg.verbose_comments}") + if cfg.save_edge is not None: + parts.append(f"'saveEdge': {cfg.save_edge}") + return '{' + ', '.join(parts) + '}' + + def options(name='Seed', generateSeedEachIteration=True, saveSeedToDatabase=False, units='si', saveRestartPeriod=None, - generateOutputHTML=False, generatePlots=False, generatePESDiagrams=False, saveSimulationProfiles=False, verboseComments=False, - saveEdgeSpecies=False, keepIrreversible=False, trimolecularProductReversible=True, wallTime='00:00:00:00', - saveSeedModulus=-1): + generateOutputHTML=False, generatePlots=False, generatePESDiagrams=False, saveSimulationProfiles=False, + verboseComments=False, saveEdgeSpecies=False, keepIrreversible=False, + trimolecularProductReversible=True, wallTime='00:00:00:00', saveSeedModulus=-1, + generateChemkin=True, generateRMSYAML=True, + generateCanteraYAML1=False, generateCanteraYAML2=False): if saveRestartPeriod: logging.warning("`saveRestartPeriod` flag was set in the input file, but this feature has been removed. Please " "remove this line from the input file. This will throw an error after RMG-Py 3.1. For " @@ -1434,7 +1489,7 @@ def options(name='Seed', generateSeedEachIteration=True, saveSeedToDatabase=Fals rmg.units = units if generateOutputHTML: logging.warning('Generate Output HTML option was turned on. Note that this will slow down model generation.') - rmg.generate_output_html = generateOutputHTML + rmg.generate_output_html = bool(generateOutputHTML) rmg.generate_plots = generatePlots rmg.generate_PES_diagrams = generatePESDiagrams if generatePESDiagrams: @@ -1450,6 +1505,12 @@ def options(name='Seed', generateSeedEachIteration=True, saveSeedToDatabase=Fals rmg.walltime = wallTime rmg.save_seed_modulus = saveSeedModulus + rmg.chemkin_writer_config = _parse_writer_config(generateChemkin) + rmg.rms_writer_config = _parse_writer_config(generateRMSYAML) + rmg.cantera1_writer_config = _parse_writer_config(generateCanteraYAML1) + rmg.cantera2_writer_config = _parse_writer_config(generateCanteraYAML2) + rmg.html_writer_config = _parse_writer_config(generateOutputHTML) + def generated_species_constraints(**kwargs): valid_constraints = [ @@ -1937,7 +1998,7 @@ def formula(elements): # Options f.write('options(\n') f.write(' units = "{0}",\n'.format(rmg.units)) - f.write(' generateOutputHTML = {0},\n'.format(rmg.generate_output_html)) + f.write(' generateOutputHTML = {0},\n'.format(_writer_config_to_input(rmg.html_writer_config))) f.write(' generatePlots = {0},\n'.format(rmg.generate_plots)) f.write(' generatePESDiagrams = {0},\n'.format(rmg.generate_PES_diagrams)) f.write(' saveSimulationProfiles = {0},\n'.format(rmg.save_simulation_profiles)) @@ -1946,6 +2007,12 @@ def formula(elements): f.write(' trimolecularProductReversible = {0},\n'.format(rmg.trimolecular_product_reversible)) f.write(' verboseComments = {0},\n'.format(rmg.verbose_comments)) f.write(' wallTime = {0},\n'.format(rmg.walltime)) + f.write(' generateChemkin = {0},\n'.format(_writer_config_to_input(rmg.chemkin_writer_config))) + f.write(' generateRMSYAML = {0},\n'.format(_writer_config_to_input(rmg.rms_writer_config))) + if rmg.cantera1_writer_config and rmg.cantera1_writer_config.enabled: + f.write(' generateCanteraYAML1 = {0},\n'.format(_writer_config_to_input(rmg.cantera1_writer_config))) + if rmg.cantera2_writer_config and rmg.cantera2_writer_config.enabled: + f.write(' generateCanteraYAML2 = {0},\n'.format(_writer_config_to_input(rmg.cantera2_writer_config))) f.write(')\n\n') f.close() diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index c26303f72dd..2d10ddaacb7 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -76,7 +76,7 @@ from rmgpy.rmg.output import OutputHTMLWriter from rmgpy.rmg.pdep import PDepNetwork from rmgpy.rmg.reactionmechanismsimulator_reactors import Reactor as RMSReactor -from rmgpy.rmg.settings import ModelSettings +from rmgpy.rmg.settings import ModelSettings, WriterConfig from rmgpy.solver.base import TerminationTime from rmgpy.stats import ExecutionStatsWriter from rmgpy.thermo.thermoengine import submit @@ -146,8 +146,14 @@ class RMG(util.Subject): `generate_output_html` ``True`` to draw pictures of the species and reactions, saving a visualized model in an output HTML file. ``False`` otherwise `generate_plots` ``True`` to generate plots of the job execution statistics after each iteration, ``False`` otherwise `generate_PES_diagrams` ``True`` to generate potential energy surface diagrams for pressure dependent networks in the model, ``False`` otherwise - `verbose_comments` ``True`` to keep the verbose comments for database estimates, ``False`` otherwise - `save_edge_species` ``True`` to save chemkin and HTML files of the edge species, ``False`` otherwise + `verbose_comments` ``True`` to keep the verbose comments for database estimates, ``False`` otherwise (global fallback when writer config does not override) + `save_edge_species` ``True`` to save chemkin and HTML files of the edge species, ``False`` otherwise (global fallback when writer config does not override) + `chemkin_writer_config` :class:`WriterConfig` controlling when the Chemkin writer runs and its per-writer options + `rms_writer_config` :class:`WriterConfig` controlling when the RMS YAML writer runs and its per-writer options + `cantera1_writer_config` :class:`WriterConfig` controlling when CanteraWriter1 runs and its per-writer options + `cantera2_writer_config` :class:`WriterConfig` controlling when CanteraWriter2 runs and its per-writer options + `html_writer_config` :class:`WriterConfig` controlling when the HTML writer runs and its per-writer options + `is_final_save` Set to ``True`` immediately before the end-of-run ``save_everything()`` call so writers know it is the final notification `keep_irreversible` ``True`` to keep ireversibility of library reactions as is ('<=>' or '=>'). ``False`` (default) to force all library reactions to be reversible ('<=>') `trimolecular_product_reversible` ``True`` (default) to allow families with trimolecular products to react in the reverse direction, ``False`` otherwise `pressure_dependence` Whether to process unimolecular (pressure-dependent) reaction networks @@ -231,6 +237,12 @@ def clear(self): self.save_simulation_profiles = None self.verbose_comments = None self.save_edge_species = None + self.chemkin_writer_config = None + self.rms_writer_config = None + self.cantera1_writer_config = None + self.cantera2_writer_config = None + self.html_writer_config = None + self.is_final_save = False self.keep_irreversible = None self.trimolecular_product_reversible = None self.pressure_dependence = None @@ -259,6 +271,19 @@ def clear(self): self.exec_time = [] self.liquid_volumetric_mass_transfer_coefficient_power_law = None + @staticmethod + def _parse_walltime_to_seconds(walltime): + """ + Convert walltime string DD:HH:MM:SS to seconds. + """ + data = walltime.split(":") + if len(data) != 4: + raise ValueError("Invalid format for wall time {0}; should be DD:HH:MM:SS.".format(walltime)) + try: + return int(data[-1]) + 60 * int(data[-2]) + 3600 * int(data[-3]) + 86400 * int(data[-4]) + except ValueError as exc: + raise ValueError("Invalid format for wall time {0}; should be DD:HH:MM:SS.".format(walltime)) from exc + def load_input(self, path=None): """ Load an RMG job from the input file located at `input_file`, or @@ -570,6 +595,24 @@ def initialize(self, **kwargs): ) ) + if "walltime" in kwargs: + logging.info( + "Overriding walltime from input file (%s) with command-line value (%s).", + self.walltime, + kwargs["walltime"], + ) + self.walltime = kwargs["walltime"] + + if "max_iterations" in kwargs: + logging.info( + "Overriding max_iterations from input file (%s) with command-line value (%s).", + self.max_iterations, + kwargs["max_iterations"], + ) + self.max_iterations = kwargs["max_iterations"] + + self.walltime = self._parse_walltime_to_seconds(self.walltime) + # Auto-select libraries if any field uses 'auto' or '' auto_select_libraries(self) @@ -647,21 +690,6 @@ def initialize(self, **kwargs): if reaction_system.T: reaction_system.viscosity = solvent_data.get_solvent_viscosity(reaction_system.T.value_si) - try: - self.walltime = kwargs["walltime"] - except KeyError: - pass - - try: - self.max_iterations = kwargs["max_iterations"] - except KeyError: - pass - - data = self.walltime.split(":") - if not len(data) == 4: - raise ValueError("Invalid format for wall time {0}; should be DD:HH:MM:SS.".format(self.walltime)) - self.walltime = int(data[-1]) + 60 * int(data[-2]) + 3600 * int(data[-3]) + 86400 * int(data[-4]) - # Initialize reaction model for spec in self.initial_species: @@ -784,13 +812,22 @@ def register_listeners(self, requires_rms=False): found in the RMG input file. """ - self.attach(ChemkinWriter(self.output_directory)) - - self.attach(RMSWriter(self.output_directory)) - self.attach(CanteraWriter1(self.output_directory)) - self.attach(CanteraWriter2(self.output_directory)) - if self.generate_output_html: - self.attach(OutputHTMLWriter(self.output_directory)) + cfg_chemkin = self.chemkin_writer_config or WriterConfig(save_interval=1) + cfg_rms = self.rms_writer_config or WriterConfig(save_interval=1) + cfg_cantera1 = self.cantera1_writer_config or WriterConfig(save_interval=0) + cfg_cantera2 = self.cantera2_writer_config or WriterConfig(save_interval=0) + cfg_html = self.html_writer_config or WriterConfig(save_interval=0) + + if cfg_chemkin.enabled: + self.attach(ChemkinWriter(self.output_directory, cfg_chemkin)) + if cfg_rms.enabled: + self.attach(RMSWriter(self.output_directory, cfg_rms)) + if cfg_cantera1.enabled: + self.attach(CanteraWriter1(self.output_directory, cfg_cantera1)) + if cfg_cantera2.enabled: + self.attach(CanteraWriter2(self.output_directory, cfg_cantera2)) + if cfg_html.enabled: + self.attach(OutputHTMLWriter(self.output_directory, cfg_html)) if self.quantum_mechanics: self.attach(QMDatabaseWriter()) @@ -910,6 +947,7 @@ def execute(self, initialize=True, **kwargs): self.make_seed_mech() max_num_spcs_hit = False # default + end_early = False for q, model_settings in enumerate(self.model_settings_list): if len(self.simulator_settings_list) > 1: @@ -919,7 +957,7 @@ def execute(self, initialize=True, **kwargs): self.filter_reactions = model_settings.filter_reactions - logging.info("Beginning model generation stage {0}...\n".format(q + 1)) + logging.info(f"Beginning model generation stage {q + 1} of {len(self.model_settings_list)}.\n") self.done = False @@ -1214,7 +1252,8 @@ def execute(self, initialize=True, **kwargs): core_spec, core_reac, edge_spec, edge_reac = self.reaction_model.get_model_size() logging.info("The current model core has %s species and %s reactions" % (core_spec, core_reac)) logging.info("The current model edge has %s species and %s reactions" % (edge_spec, edge_reac)) - return + end_early = True + break if self.max_iterations and (self.reaction_model.iteration_num >= self.max_iterations): logging.info("MODEL GENERATION TERMINATED") @@ -1225,75 +1264,89 @@ def execute(self, initialize=True, **kwargs): core_spec, core_reac, edge_spec, edge_reac = self.reaction_model.get_model_size() logging.info("The current model core has %s species and %s reactions" % (core_spec, core_reac)) logging.info("The current model edge has %s species and %s reactions" % (edge_spec, edge_reac)) - return + end_early = True + break if max_num_spcs_hit: # resets maxNumSpcsHit and continues the settings for loop logging.info("The maximum number of species ({0}) has been hit, Exiting stage {1} ...".format(model_settings.max_num_species, q + 1)) max_num_spcs_hit = False + if end_early: # breaks the settings for loop + break + # Save the final seed mechanism self.make_seed_mech() + # Notify all writers that this is the final save (end-of-run). + # Writers configured with saveInterval=-1 will write only here. + # Writers configured with saveInterval>0 will also write here + # unless they already wrote on this iteration. + self.is_final_save = True + self.save_everything() + self.is_final_save = False + self.run_model_analysis() # generate Cantera files chem.yaml & chem_annotated.yaml in designated Cantera output folders try: - logging.info("Translating final chemkin file into Cantera yaml.") translated_cantera_file = None - if any([s.contains_surface_site() for s in self.reaction_model.core.species]): - # Surface (catalytic) chemistry - translated_cantera_file = self.generate_cantera_files_from_chemkin( - os.path.join(self.output_directory, "chemkin", "chem-gas.inp"), - surface_file=(os.path.join(self.output_directory, "chemkin", "chem-surface.inp")), - ) - self.generate_cantera_files_from_chemkin( - os.path.join(self.output_directory, "chemkin", "chem_annotated-gas.inp"), - surface_file=(os.path.join(self.output_directory, "chemkin", "chem_annotated-surface.inp")), - ) + if self.chemkin_writer_config and self.chemkin_writer_config.enabled: + logging.info("Translating final chemkin file into Cantera yaml.") + if any([s.contains_surface_site() for s in self.reaction_model.core.species]): + # Surface (catalytic) chemistry + translated_cantera_file = self.generate_cantera_files_from_chemkin( + os.path.join(self.output_directory, "chemkin", "chem-gas.inp"), + surface_file=(os.path.join(self.output_directory, "chemkin", "chem-surface.inp")), + ) + self.generate_cantera_files_from_chemkin( + os.path.join(self.output_directory, "chemkin", "chem_annotated-gas.inp"), + surface_file=(os.path.join(self.output_directory, "chemkin", "chem_annotated-surface.inp")), + ) - if self.thermo_coverage_dependence: - # Build coverage_deps: {species_name: string_to_add_to_yaml} - coverage_deps = {} - for s in self.reaction_model.core.species: - if s.contains_surface_site() and s.thermo.thermo_coverage_dependence: - s_name = s.to_chemkin() - for dep_sp_adj, parameters in s.thermo.thermo_coverage_dependence.items(): - mol = Molecule().from_adjacency_list(dep_sp_adj) - for sp in self.reaction_model.core.species: - if sp.is_isomorphic(mol, strict=False): - if s_name not in coverage_deps: - coverage_deps[s_name] = ' coverage-dependencies:' - coverage_deps[s_name] += f""" + if self.thermo_coverage_dependence: + # Build coverage_deps: {species_name: string_to_add_to_yaml} + coverage_deps = {} + for s in self.reaction_model.core.species: + if s.contains_surface_site() and s.thermo.thermo_coverage_dependence: + s_name = s.to_chemkin() + for dep_sp_adj, parameters in s.thermo.thermo_coverage_dependence.items(): + mol = Molecule().from_adjacency_list(dep_sp_adj) + for sp in self.reaction_model.core.species: + if sp.is_isomorphic(mol, strict=False): + if s_name not in coverage_deps: + coverage_deps[s_name] = ' coverage-dependencies:' + coverage_deps[s_name] += f""" {sp.to_chemkin()}: model: {parameters['model']} enthalpy-coefficients: {[v.value_si for v in parameters['enthalpy-coefficients']]} entropy-coefficients: {[v.value_si for v in parameters['entropy-coefficients']]} units: {{energy: J, quantity: mol}} """ - break + break - for yaml_path in [ - os.path.join(self.output_directory, "cantera", "chem.yaml"), - os.path.join(self.output_directory, "cantera", "chem_annotated.yaml"), - ]: - _add_coverage_dependence_to_cantera_yaml(yaml_path, coverage_deps) + for yaml_path in [ + os.path.join(self.output_directory, "cantera", "chem.yaml"), + os.path.join(self.output_directory, "cantera", "chem_annotated.yaml"), + ]: + _add_coverage_dependence_to_cantera_yaml(yaml_path, coverage_deps) - else: # gas phase only - translated_cantera_file = self.generate_cantera_files_from_chemkin( - os.path.join(self.output_directory, "chemkin", "chem.inp") - ) - self.generate_cantera_files_from_chemkin( - os.path.join(self.output_directory, "chemkin", "chem_annotated.inp") - ) - - # Compare translated Cantera files and directly generated Cantera files + else: # gas phase only + translated_cantera_file = self.generate_cantera_files_from_chemkin( + os.path.join(self.output_directory, "chemkin", "chem.inp") + ) + self.generate_cantera_files_from_chemkin( + os.path.join(self.output_directory, "chemkin", "chem_annotated.inp") + ) - compare_yaml_files_and_report(translated_cantera_file, - os.path.join(self.output_directory, "cantera1", "chem.yaml"), - output=os.path.join(self.output_directory, "cantera1", "comparison_report.txt")) - compare_yaml_files_and_report(translated_cantera_file, - os.path.join(self.output_directory, "cantera2", "chem.yaml"), - output=os.path.join(self.output_directory, "cantera2", "comparison_report.txt")) + # Compare translated Cantera files against directly generated Cantera files + if translated_cantera_file and self.cantera1_writer_config and self.cantera1_writer_config.enabled: + compare_yaml_files_and_report(translated_cantera_file, + os.path.join(self.output_directory, "cantera1", "chem.yaml"), + output=os.path.join(self.output_directory, "cantera1", "comparison_report.txt")) + if translated_cantera_file and self.cantera2_writer_config and self.cantera2_writer_config.enabled: + compare_yaml_files_and_report(translated_cantera_file, + os.path.join(self.output_directory, "cantera2", "chem.yaml"), + output=os.path.join(self.output_directory, "cantera2", "comparison_report.txt")) except EnvironmentError: logging.exception("Could not generate Cantera files due to EnvironmentError. Check read\\write privileges in output directory.") @@ -1302,12 +1355,14 @@ def execute(self, initialize=True, **kwargs): self.check_model() # Write output file - logging.info("") - logging.info("MODEL GENERATION COMPLETED") - logging.info("") - core_spec, core_reac, edge_spec, edge_reac = self.reaction_model.get_model_size() - logging.info("The final model core has %s species and %s reactions" % (core_spec, core_reac)) - logging.info("The final model edge has %s species and %s reactions" % (edge_spec, edge_reac)) + + if not end_early: + logging.info("") + logging.info("MODEL GENERATION COMPLETED") + logging.info("") + core_spec, core_reac, edge_spec, edge_reac = self.reaction_model.get_model_size() + logging.info("The final model core has %s species and %s reactions" % (core_spec, core_reac)) + logging.info("The final model edge has %s species and %s reactions" % (edge_spec, edge_reac)) self.finish() diff --git a/rmgpy/rmg/output.py b/rmgpy/rmg/output.py index 93cc8782bb3..bcd40c0826f 100644 --- a/rmgpy/rmg/output.py +++ b/rmgpy/rmg/output.py @@ -1317,14 +1317,16 @@ def csssafe(input): f.close() -def save_output(rmg): +def save_output(rmg, save_edge=None): """ Save the current reaction model to a pretty HTML file. """ + if save_edge is None: + save_edge = rmg.save_edge_species logging.info('Saving current model core to HTML file...') save_output_html(os.path.join(rmg.output_directory, 'output.html'), rmg.reaction_model, 'core') - if rmg.save_edge_species: + if save_edge: logging.info('Saving current model edge to HTML file...') save_output_html(os.path.join(rmg.output_directory, 'output_edge.html'), rmg.reaction_model, 'edge') @@ -1351,9 +1353,14 @@ class OutputHTMLWriter(object): """ - def __init__(self, output_directory=''): + def __init__(self, output_directory='', config=None): super(OutputHTMLWriter, self).__init__() + self.config = config make_output_subdirectory(output_directory, 'species') def update(self, rmg): - save_output(rmg) + if self.config is not None and not self.config.should_write( + rmg.reaction_model.iteration_num, rmg.is_final_save): + return + save_edge = self.config.save_edge if (self.config and self.config.save_edge is not None) else rmg.save_edge_species + save_output(rmg, save_edge=save_edge) diff --git a/rmgpy/rmg/settings.py b/rmgpy/rmg/settings.py index b4a7bd52e0b..efe4ca8625a 100644 --- a/rmgpy/rmg/settings.py +++ b/rmgpy/rmg/settings.py @@ -139,3 +139,45 @@ def __init__(self, atol=1e-16, rtol=1e-8, sens_atol=1e-6, sens_rtol=1e-4): self.rtol = rtol self.sens_atol = sens_atol self.sens_rtol = sens_rtol + + +class WriterConfig: + """ + Configuration for a single output-format writer. + + Attributes + ---------- + save_interval : int + How often to write output. Positive N = every N iterations (0-indexed + iteration numbers, so iteration 0 is always included). -1 = end of run + only. 0 = disabled entirely. + verbose_comments : bool or None + Per-writer override for verbose comments. None means fall back to the + global ``rmg.verbose_comments``. + save_edge : bool or None + Per-writer override for saving edge species. None means fall back to + the global ``rmg.save_edge_species``. + """ + + def __init__(self, save_interval=1, verbose_comments=None, save_edge=None): + self.save_interval = save_interval + self.verbose_comments = verbose_comments + self.save_edge = save_edge + self._last_write = -1 + + @property + def enabled(self): + return self.save_interval != 0 + + def should_write(self, iteration_num, is_final): + """Return True if the writer should produce output right now.""" + if not self.enabled: + return False + if self.save_interval == -1: + return is_final + if is_final: + return self._last_write != iteration_num + result = (iteration_num % self.save_interval == 0) + if result: + self._last_write = iteration_num + return result diff --git a/rmgpy/util.py b/rmgpy/util.py index 15769e02e61..8be80d5f889 100644 --- a/rmgpy/util.py +++ b/rmgpy/util.py @@ -173,7 +173,7 @@ def parse_command_line_arguments(command_line_args=None): parser.add_argument('-P', '--postprocess', action='store_true', help='postprocess profiling statistics from previous [failed] run; does not run the simulation') - parser.add_argument('-t', '--walltime', type=str, nargs=1, default='00:00:00:00', + parser.add_argument('-t', '--walltime', type=str, nargs=1, default=None, metavar='DD:HH:MM:SS', help='set the maximum execution time') parser.add_argument('-i', '--maxiter', type=int, nargs=1, default=None, @@ -197,7 +197,7 @@ def parse_command_line_arguments(command_line_args=None): args.file = args.file[0] # If walltime was specified, retrieve this string from the element 1 list - if args.walltime != '00:00:00:00': + if args.walltime: args.walltime = args.walltime[0] if args.restart: diff --git a/rmgpy/yaml_cantera1.py b/rmgpy/yaml_cantera1.py index 5a1461f8e19..db2928b0c94 100644 --- a/rmgpy/yaml_cantera1.py +++ b/rmgpy/yaml_cantera1.py @@ -450,13 +450,19 @@ class CanteraWriter1(object): """ - def __init__(self, output_directory=""): + def __init__(self, output_directory="", config=None): super(CanteraWriter1, self).__init__() self.output_directory = output_directory + self.config = config self.output_subdirectory = os.path.join(self.output_directory, "cantera1") make_output_subdirectory(output_directory, "cantera1") def update(self, rmg): + if self.config is not None and not self.config.should_write( + rmg.reaction_model.iteration_num, rmg.is_final_save): + return + verbose = self.config.verbose_comments if (self.config and self.config.verbose_comments is not None) else rmg.verbose_comments + save_edge = self.config.save_edge if (self.config and self.config.save_edge is not None) else rmg.save_edge_species num_species = len(rmg.reaction_model.core.species) this_output_path = os.path.join(self.output_subdirectory, @@ -483,7 +489,7 @@ def update(self, rmg): ) shutil.copy2(this_output_path, latest_output_path) - if rmg.verbose_comments: + if verbose: annotated_path = os.path.join(self.output_subdirectory, 'chem_annotated.yaml') logging.info(f"Saving annotated Cantera file: {annotated_path}") write_cantera( @@ -496,7 +502,7 @@ def update(self, rmg): verbose=True, ) - if rmg.save_edge_species: + if save_edge: logging.info('Saving current model core and edge to Cantera file...') edge_species = rmg.reaction_model.core.species + rmg.reaction_model.edge.species edge_reactions = rmg.reaction_model.core.reactions + rmg.reaction_model.edge.reactions @@ -515,7 +521,7 @@ def update(self, rmg): ) shutil.copy2(this_edge_path, latest_edge_path) - if rmg.verbose_comments: + if verbose: annotated_edge_path = os.path.join(self.output_subdirectory, 'chem_edge_annotated.yaml') logging.info(f"Saving annotated edge Cantera file: {annotated_edge_path}") diff --git a/rmgpy/yaml_cantera2.py b/rmgpy/yaml_cantera2.py index 1155f001dd4..b5fd10500f7 100644 --- a/rmgpy/yaml_cantera2.py +++ b/rmgpy/yaml_cantera2.py @@ -77,26 +77,33 @@ class CanteraWriter2(object): with the current state of the RMG model at every iteration. """ - def __init__(self, output_directory=''): + def __init__(self, output_directory='', config=None): self.output_directory = output_directory + self.config = config make_output_subdirectory(output_directory, 'cantera2') def update(self, rmg): """ Called whenever the RMG subject notifies listeners. """ - save_cantera_files(rmg) + if self.config is not None and not self.config.should_write( + rmg.reaction_model.iteration_num, rmg.is_final_save): + return + save_cantera_files(rmg, config=self.config) -def save_cantera_files(rmg): +def save_cantera_files(rmg, config=None): """ Save the current reaction model to a set of Cantera YAML files. Creates: 1. chem{N}.yaml (where N is num species) 2. chem.yaml (latest copy) - 3. chem_annotated.yaml (if rmg.verbose_comments is True) + 3. chem_annotated.yaml (if verbose_comments is True) """ + verbose = config.verbose_comments if (config and config.verbose_comments is not None) else rmg.verbose_comments + save_edge = config.save_edge if (config and config.save_edge is not None) else rmg.save_edge_species + # Ensure subdirectory exists cantera_dir = os.path.join(rmg.output_directory, 'cantera2') if not os.path.exists(cantera_dir): @@ -129,7 +136,7 @@ def save_cantera_files(rmg): shutil.copy2(this_cantera_path, latest_cantera_path) # Write annotated file if verbose_comments is requested - if rmg.verbose_comments: + if verbose: annotated_path = os.path.join(cantera_dir, 'chem_annotated.yaml') logging.info(f"Saving annotated Cantera file: {annotated_path}") save_cantera_model(rmg.reaction_model.core, annotated_path, site_density=site_density, @@ -138,7 +145,7 @@ def save_cantera_files(rmg): # ------------------------------------------------------------------------- # 2. Save Edge Model (Optional, matching ChemkinWriter logic) # ------------------------------------------------------------------------- - if rmg.save_edge_species: + if save_edge: logging.info('Saving current model core and edge to Cantera file...') this_edge_path = os.path.join(cantera_dir, @@ -162,7 +169,7 @@ def __init__(self, species, reactions): os.unlink(latest_edge_path) shutil.copy2(this_edge_path, latest_edge_path) - if rmg.verbose_comments: + if verbose: annotated_edge_path = os.path.join(cantera_dir, 'chem_edge_annotated.yaml') logging.info(f"Saving annotated edge Cantera file: {annotated_edge_path}") save_cantera_model(edge_model, annotated_edge_path, site_density=site_density, diff --git a/rmgpy/yaml_rms.py b/rmgpy/yaml_rms.py index 8bc0df39d06..4132b0ae053 100644 --- a/rmgpy/yaml_rms.py +++ b/rmgpy/yaml_rms.py @@ -274,12 +274,16 @@ class RMSWriter(object): rmg.detach(listener) """ - def __init__(self, output_directory=''): + def __init__(self, output_directory='', config=None): super(RMSWriter, self).__init__() self.output_directory = output_directory + self.config = config make_output_subdirectory(output_directory, 'rms') def update(self, rmg): + if self.config is not None and not self.config.should_write( + rmg.reaction_model.iteration_num, rmg.is_final_save): + return solvent_data = None if rmg.solvent: solvent_data = rmg.database.solvation.get_solvent_data(rmg.solvent) diff --git a/test/rmgpy/rmg/inputTest.py b/test/rmgpy/rmg/inputTest.py index f9b083822aa..b0fe108b675 100644 --- a/test/rmgpy/rmg/inputTest.py +++ b/test/rmgpy/rmg/inputTest.py @@ -31,8 +31,10 @@ import rmgpy.rmg.input as inp from rmgpy.exceptions import InputError +from rmgpy.rmg.input import _parse_writer_config, _writer_config_to_input from rmgpy.rmg.main import RMG from rmgpy.rmg.model import CoreEdgeReactionModel +from rmgpy.rmg.settings import WriterConfig from rmgpy.ml.estimator import ADMONITION import pytest @@ -538,3 +540,95 @@ def test_completed_networks_none(self): # Check that no networks were added assert len(rmg.reaction_model.completed_pdep_networks) == 0 + + +class TestWriterConfig: + """Unit tests for WriterConfig and the _parse_writer_config / _writer_config_to_input helpers.""" + + def test_parse_false_disables(self): + cfg = _parse_writer_config(False) + assert not cfg.enabled + assert cfg.save_interval == 0 + + def test_parse_true_enables_default_interval(self): + cfg = _parse_writer_config(True) + assert cfg.enabled + assert cfg.save_interval == 1 + assert cfg.verbose_comments is None + assert cfg.save_edge is None + + def test_parse_true_custom_default_interval(self): + cfg = _parse_writer_config(True, default_save_interval=5) + assert cfg.save_interval == 5 + + def test_parse_dict_full(self): + cfg = _parse_writer_config({'saveInterval': -1, 'verboseComments': True, 'saveEdge': False}) + assert cfg.enabled + assert cfg.save_interval == -1 + assert cfg.verbose_comments is True + assert cfg.save_edge is False + + def test_parse_dict_partial(self): + cfg = _parse_writer_config({'saveInterval': 3}) + assert cfg.save_interval == 3 + assert cfg.verbose_comments is None + assert cfg.save_edge is None + + def test_parse_invalid_raises(self): + with pytest.raises(InputError): + _parse_writer_config(42) + + def test_should_write_every_iteration(self): + cfg = WriterConfig(save_interval=1) + assert cfg.should_write(0, False) + assert cfg.should_write(1, False) + assert cfg.should_write(5, False) + + def test_should_write_every_n_iterations(self): + cfg = WriterConfig(save_interval=3) + assert cfg.should_write(0, False) + assert not cfg.should_write(1, False) + assert not cfg.should_write(2, False) + assert cfg.should_write(3, False) + assert cfg.should_write(6, False) + + def test_should_write_end_only(self): + cfg = WriterConfig(save_interval=-1) + assert not cfg.should_write(0, False) + assert not cfg.should_write(5, False) + assert cfg.should_write(5, True) + + def test_should_write_disabled(self): + cfg = WriterConfig(save_interval=0) + assert not cfg.should_write(0, False) + assert not cfg.should_write(0, True) + + def test_should_write_final_always_writes(self): + cfg = WriterConfig(save_interval=5) + # Iteration 7 was not written (not multiple of 5) + assert cfg.should_write(7, True) + + def test_should_write_final_no_double_write(self): + cfg = WriterConfig(save_interval=5) + # Iteration 5 is a multiple of 5 — written during loop + cfg.should_write(5, False) + # Final call at same iteration should be skipped + assert not cfg.should_write(5, True) + + def test_writer_config_to_input_false(self): + cfg = WriterConfig(save_interval=0) + assert _writer_config_to_input(cfg) is False + + def test_writer_config_to_input_true(self): + cfg = WriterConfig(save_interval=1) + assert _writer_config_to_input(cfg) is True + + def test_writer_config_to_input_dict(self): + cfg = WriterConfig(save_interval=-1, verbose_comments=True, save_edge=False) + result = _writer_config_to_input(cfg) + assert "'saveInterval': -1" in result + assert "'verboseComments': True" in result + assert "'saveEdge': False" in result + + def test_writer_config_to_input_none(self): + assert _writer_config_to_input(None) is False diff --git a/test/rmgpy/rmg/rmgTest.py b/test/rmgpy/rmg/rmgTest.py index 26686ae2c18..1fab407c8ee 100644 --- a/test/rmgpy/rmg/rmgTest.py +++ b/test/rmgpy/rmg/rmgTest.py @@ -206,7 +206,7 @@ def test_parse_command_line_arguments_defaults(self): args = parse_command_line_arguments(["input.py"]) # Test default values - assert args.walltime == "00:00:00:00" + assert args.walltime is None assert args.output_directory == os.path.abspath(os.path.dirname("./")) assert args.debug == False assert args.file == "input.py"