Source code for uclchem.makerates.makerates

"""UCLCHEM MakeRates."""

import logging
import os
from pathlib import Path
from typing import Literal

from uclchem.makerates import io_functions as io
from uclchem.makerates._output_resolver import resolve_output_dirs
from uclchem.makerates.config import MakeratesConfig
from uclchem.makerates.network import Network

[docs] logger = logging.getLogger(__name__)
# Logging verbosity levels accepted by ``get_network``.
[docs] LogLevel = Literal[10, 20, 30, 40, 50]
# Optional parameters that don't raise errors if missing
[docs] optional_params = [ "grain_assisted_recombination_file", "output_directory", "three_phase", "gas_phase_extrapolation", ]
[docs] def run_makerates( configuration_file: str | bytes | Path | MakeratesConfig = "user_settings.yaml", write_files: bool = True, output_directory: str | os.PathLike | None = None, ) -> Network: """Run makerates. Main run wrapper for makerates. Loads and validates configuration, generates chemical network, and optionally writes output files. Parameters ---------- configuration_file : str | bytes | Path | MakeratesConfig Path to YAML configuration file, or just a configuration. Defaults to "user_settings.yaml". write_files : bool Whether to write fortran files to src/fortran_src. Defaults to True. output_directory : str | os.PathLike | None Optional override for the output directory where files should be written. If None, uses the 'output_directory' from the config (if present) or the package defaults. Returns ------- network : Network A validated chemical network instance. Raises ------ ValueError If `coolants_file` is a directory, and not a path to a file. """ if isinstance(configuration_file, MakeratesConfig): config = configuration_file else: # Load and validate configuration using Pydantic config_path: str | Path = ( configuration_file.decode() if isinstance(configuration_file, bytes) else configuration_file ) config = MakeratesConfig.from_yaml(config_path) # Log the configuration config.log_configuration() # Resolve output directories using tiered priority: # 1) explicit kwarg, 2) config field, 3) stored project root, 4) legacy relative paths explicit_dir = output_directory or config.output_directory if explicit_dir: # Ensure the directory exists explicit_dir = Path(explicit_dir) if not explicit_dir.is_dir(): explicit_dir.mkdir(parents=True) output_dir, fortran_src_dir = resolve_output_dirs( explicit_dir, use_legacy_relative=True, ) # Get all reaction files and types reaction_files = config.get_all_reaction_files() reaction_types = config.get_all_reaction_types() # Resolve species file path species_file = config.resolve_path(config.species_file) # Resolve GAR file if present gar_file = None if config.grain_assisted_recombination_file: gar_file = config.resolve_path(config.grain_assisted_recombination_file) # Resolve exothermicity files if present database_reaction_exothermicity = None if config.database_reaction_exothermicity: database_reaction_exothermicity = [ config.resolve_path(ef) for ef in config.database_reaction_exothermicity ] # Retrieve the network and the dropped reactions network, dropped_reactions = _get_network_from_files( reaction_files=reaction_files, reaction_types=reaction_types, species_file=species_file, gas_phase_extrapolation=config.gas_phase_extrapolation, add_crp_photo_to_grain=config.add_crp_photo_to_grain, derive_reaction_exothermicity=config.derive_reaction_exothermicity, database_reaction_exothermicity=database_reaction_exothermicity, ) # Determine which coolants to write. Precedence (highest -> lowest): # 1) inline `coolants` in config, 2) `coolants_file` referenced in config, # 3) defaults used by write_outputs. coolants_to_write = None if config.coolants is not None: logger.info(f"Using {len(config.coolants)} inline coolants from config") coolants_to_write = config.coolants elif config.coolants_file: coolants_path = config.resolve_path(config.coolants_file) # Defensive check: don't try to read a directory as a YAML file if coolants_path.is_dir(): msg = ( f"coolants_file {coolants_path} resolves to a directory; expected a YAML file listing coolants. " "If you intended to set the collisional rate data directory, use 'coolant_data_dir' in your config." ) raise ValueError(msg) try: _coolants = io.read_coolants_file(coolants_path) logger.info(f"Loaded {len(_coolants)} coolants from {coolants_path}") coolants_to_write = _coolants except Exception as exc: msg = f"Error reading coolants_file {coolants_path}: {exc}" raise ValueError(msg) from exc if write_files: logger.info( "\n################################################\n" + "Checks complete, writing output files\n" + "################################################\n" ) # Write dropped reactions io.output_drops( dropped_reactions=dropped_reactions, output_dir=output_dir, write_files=write_files, ) logger.info(f"There are {len(dropped_reactions)} dropped reactions") # Check for GAR reactions and validate parameters gar_reactions = network.get_reactions_by_types("GAR") gar_parameters = None if len(gar_reactions) > 0: if gar_file is None: msg = ( "You have GAR reactions in your network, but you did " "not specify a grain_assisted_recombination_file in " "your configuration. Refer to makerates documentation." ) raise ValueError(msg) # Get all the individual ions that can recombine gar_ions = [gar.get_reactants()[0] for gar in gar_reactions] _gar_parameters = io.read_grain_assisted_recombination_file(gar_file) if not set(gar_ions).issubset(set(_gar_parameters.keys())): missing_ions = set(gar_ions) - set(_gar_parameters.keys()) msg = ( f"You have GAR reactions for ions {missing_ions} but " f"they are not defined in your gar_file {gar_file}" ) raise ValueError(msg) # Save the gar parameters in the correct order gar_parameters = {ion: _gar_parameters[ion] for ion in gar_ions} # Pass resolved output directories and other parameters to write_outputs io.write_outputs( network, output_dir, fortran_src_dir, enable_rates_storage=config.enable_rates_storage, gar_database=gar_parameters, coolants=coolants_to_write, coolant_data_dir=config.coolant_data_dir, ) # Copy coolant data files to package data directory for installation # Only pass coolant_data_dir if it's explicitly set and valid source_dir = ( config.coolant_data_dir if config.coolant_data_dir and config.coolant_data_dir != "." else None ) io.copy_coolant_files(source_dir=source_dir) ngrain = len([x for x in network.get_species_list() if x.is_surface_species()]) logger.info(f"Total number of species = {len(network.get_species_list())}") logger.info(f"Number of surface species = {ngrain}") logger.info(f"Number of reactions = {len(network.get_reaction_list())}") # Return the network for reuse in code/notebooks return network
[docs] def get_network( path_to_input_file: str | bytes | Path | None = None, path_to_species_file: str | bytes | Path | None = None, path_to_reaction_file: str | bytes | Path | None = None, verbosity: LogLevel | None = None, ) -> Network: """Get a network into memory. In memory equivalent of Makerates, can either be used on the original input files for makerates, or on the output files that makerates generates. So either specify: `path_to_input_file ` exclusive OR (`path_to_species_file` and `path_to_reaction_file`) The latter scenario allows you to reload a reaction network from a network already written by Makerates. Parameters ---------- path_to_input_file : str | bytes | Path | None Path to input file. Defaults to None. path_to_species_file : str | bytes | Path | None Path to a species.csv in/from the src directory. Defaults to None. path_to_reaction_file : str | bytes | Path | None Path to a reactions.csv in/from the src directory. Defaults to None. verbosity : LogLevel | None The verbosity level as specified in ``logging`` (e.g. ``logging.DEBUG``, ``logging.INFO``, ``logging.WARNING``, ``logging.CRITICAL``, ``logging.ERROR``). Defaults to None. Returns ------- Network A chemical reaction network. Raises ------ ValueError You cannot specify both an input configuration and species+reaction. """ if verbosity: logging.basicConfig(format="%(levelname)s: %(message)s", level=verbosity) if bool(path_to_input_file) and bool(path_to_species_file or path_to_reaction_file): msg = "Cannot have both an input Makerates config file and explicit paths to species + reaction files" raise ValueError(msg) if path_to_input_file: return run_makerates(path_to_input_file, write_files=False) else: # If we load the species/reactions directly from UCLCHEM we can skip the checks return Network.from_csv(path_to_species_file, path_to_reaction_file)
def _get_network_from_files( species_file: Path, reaction_files: list[Path], reaction_types: list[Literal["UMIST", "KIDA", "UCL"]], gas_phase_extrapolation: bool, add_crp_photo_to_grain: bool, derive_reaction_exothermicity: bool | str | list[str], database_reaction_exothermicity: list[Path] | None = None, ) -> tuple[Network, list[list[str]]]: logger.info( f"_get_network_from_files called with database_reaction_exothermicity={database_reaction_exothermicity}" ) species_list, user_defined_bulk = io.read_species_file(species_file) # Check if reaction and type files are lists, if not, make them lists if not isinstance(reaction_files, list): reaction_files = [reaction_files] if not isinstance(reaction_types, list): reaction_types = [reaction_types] reactions = [] dropped_reactions = [] # Support an arbitrary amount of different reaction files and append then in the end. for reaction_file, reaction_type in zip(reaction_files, reaction_types, strict=False): temp_reactions, temp_dropped_reactions = io.read_reaction_file( reaction_file, species_list, reaction_type ) reactions += temp_reactions dropped_reactions += temp_dropped_reactions # Create Network using the build() factory method network = Network.build( species=species_list, reactions=reactions, user_defined_bulk=user_defined_bulk, gas_phase_extrapolation=gas_phase_extrapolation, add_crp_photo_to_grain=add_crp_photo_to_grain, derive_reaction_exothermicity=derive_reaction_exothermicity, database_reaction_exothermicity=database_reaction_exothermicity, ) ################################################################################################# logger.info( "\n################################################\n" + "Reading and checking input\n" + "################################################\n" ) # Network checking is now done automatically during build # in NetworkBuilder._check_network() return network, dropped_reactions