"""Unified Network implementation for UCLCHEM.
This module provides the Network class and factory functions for creating networks
in different contexts:
- load_network_from_csv(): Load compiled networks for analysis
- build_network(): Build new networks with full validation
- create_network(): Direct instantiation from objects
The Network class implements a complete interface for accessing and modifying
chemical reaction networks, suitable for build-time and analysis-time use.
For runtime parameter modification during model execution, use RuntimeNetwork
from uclchem.advanced.runtime_network instead.
"""
import logging
from abc import ABC, abstractmethod
from copy import deepcopy
from pathlib import Path
from uclchem.utils import UCLCHEM_ROOT_DIR, get_reaction_table, get_species_table
from .reaction import REACTION_TYPES, CoupledReaction, Reaction
from .species import Species
[docs]
logger = logging.getLogger(__name__)
# ============================================================================
# Abstract Base Classes
# ============================================================================
[docs]
class NetworkABC(ABC):
"""Base abstract class defining the read-only network interface.
Defines operations common to ALL network types: reading data, querying,
and modifying existing parameters. Does NOT include add/remove operations
since some implementations (like RuntimeNetwork) have fixed structure.
This interface is implemented by:
- Network: Full interactive network (via MutableNetworkABC)
- RuntimeNetwork: Fortran-backed runtime network (read + parameter modification only)
"""
# Core Properties
@property
@abstractmethod
[docs]
def species(self) -> dict[str, Species]:
"""Get the species collection."""
pass
@property
@abstractmethod
[docs]
def reactions(self) -> dict[int, Reaction]:
"""Get the reactions collection."""
pass
# Species Read Interface
@abstractmethod
[docs]
def get_species_list(self) -> list[Species]:
"""Get all species in the network."""
pass
@abstractmethod
[docs]
def get_species_dict(self) -> dict[str, Species]:
"""Get the species dictionary."""
pass
@abstractmethod
[docs]
def get_specie(self, specie_name: str) -> Species:
"""Get a specific species by name.
Parameters
----------
specie_name : str
Name of the species.
"""
pass
# Reaction Read Interface
@abstractmethod
[docs]
def get_reaction_list(self) -> list[Reaction]:
"""Get all reactions in the network."""
pass
@abstractmethod
[docs]
def get_reaction_dict(self) -> dict[int, Reaction]:
"""Get the reactions dictionary."""
pass
@abstractmethod
[docs]
def get_reaction(self, reaction_idx: int) -> Reaction:
"""Get a specific reaction by index.
Parameters
----------
reaction_idx : int
Index of the reaction in the network.
"""
pass
# Query Methods
@abstractmethod
[docs]
def get_reactions_by_types(self, reaction_type: str | list[str]) -> list[Reaction]:
"""Get all reactions of specific type(s).
Parameters
----------
reaction_type : str | list[str]
Reaction type label to filter on (e.g. ``'MA'``, ``'DR'``).
"""
pass
@abstractmethod
[docs]
def find_similar_reactions(self, reaction: Reaction) -> dict[int, Reaction]:
"""Find reactions with same reactants/products.
Parameters
----------
reaction : Reaction
Reaction instance to look up or modify.
"""
pass
@abstractmethod
[docs]
def get_reaction_index(self, reaction: Reaction) -> int:
"""Get the unique index of a reaction.
Parameters
----------
reaction : Reaction
Reaction instance to look up or modify.
"""
pass
# Parameter Modification Methods (modify existing, don't add/remove)
@abstractmethod
[docs]
def change_binding_energy(self, specie: str, new_binding_energy: float) -> None:
"""Change binding energy of an existing species.
Parameters
----------
specie : str
Name of the species.
new_binding_energy : float
New binding energy in Kelvin.
"""
pass
@abstractmethod
[docs]
def change_reaction_barrier(self, reaction: Reaction, barrier: float) -> None:
"""Change activation barrier of an existing reaction.
Parameters
----------
reaction : Reaction
Reaction instance to look up or modify.
barrier : float
New reaction barrier in Kelvin.
"""
pass
def __repr__(self) -> str:
"""Return a string representation of the network.
Returns
-------
str
String representation of network.
"""
n_species = len(self.get_species_list())
n_reactions = len(self.get_reaction_list())
return (
f"{self.__class__.__name__}(species={n_species},\n reactions={n_reactions})"
)
[docs]
class MutableNetworkABC(NetworkABC):
"""Extended interface for networks that support full CRUD operations.
Adds add/remove/set operations for species and reactions on top of the
base NetworkABC interface. Used by the Network class for build-time and
analysis-time operations.
NOT implemented by RuntimeNetwork since the Fortran-backed network is read
directly from the fortran files, only allowing for the editing of existing reaction
and species parameters.
"""
# Species Modification Interface
@abstractmethod
[docs]
def add_species(self, species: Species | list) -> None:
"""Add one or more species to the network.
Parameters
----------
species : Species | list
Species instance or list of species to add.
"""
pass
@abstractmethod
[docs]
def remove_species(self, specie_name: str) -> None:
"""Remove a species from the network.
Parameters
----------
specie_name : str
Name of the species.
"""
pass
@abstractmethod
[docs]
def set_specie(self, species_name: str, species: Species) -> None:
"""Set/update a species in the network.
Parameters
----------
species_name : str
Name of the species.
species : Species
Species instance or list of species to add.
"""
pass
@abstractmethod
[docs]
def set_species_dict(self, new_species_dict: dict[str, Species]) -> None:
"""Replace the entire species dictionary.
Parameters
----------
new_species_dict : dict[str, Species]
Replacement species dictionary.
"""
pass
@abstractmethod
[docs]
def sort_species(self) -> None:
"""Sort species by type and mass."""
pass
# Reaction Modification Interface
@abstractmethod
[docs]
def add_reactions(self, reactions: Reaction | list) -> None:
"""Add one or more reactions to the network.
Parameters
----------
reactions : Reaction | list
Reactions to add to the network.
"""
pass
@abstractmethod
[docs]
def remove_reaction(self, reaction: Reaction) -> None:
"""Remove a reaction from the network.
Parameters
----------
reaction : Reaction
Reaction instance to look up or modify.
"""
pass
@abstractmethod
[docs]
def remove_reaction_by_index(self, reaction_idx: int) -> None:
"""Remove a reaction by its index.
Parameters
----------
reaction_idx : int
Index of the reaction in the network.
"""
pass
@abstractmethod
[docs]
def set_reaction(self, reaction_idx: int, reaction: Reaction) -> None:
"""Set/update a reaction at a specific index.
Parameters
----------
reaction_idx : int
Index of the reaction in the network.
reaction : Reaction
Reaction instance to look up or modify.
"""
pass
@abstractmethod
[docs]
def set_reaction_dict(self, new_dict: dict[int, Reaction]) -> None:
"""Replace the entire reaction dictionary.
Parameters
----------
new_dict : dict[int, Reaction]
Replacement reactions dictionary.
"""
pass
@abstractmethod
[docs]
def sort_reactions(self) -> None:
"""Sort reactions by type and reactants."""
pass
# ============================================================================
# Base Network Implementation
# ============================================================================
[docs]
class BaseNetwork(NetworkABC):
"""Base implementation providing common network operations.
Implements all read and query operations that are common between
Network and RuntimeNetwork. Both classes store data in _species_dict
and _reactions_dict, so this base class can implement all the shared logic.
Subclasses only need to:
1. Initialize _species_dict and _reactions_dict
2. Implement modification methods (change_binding_energy, change_reaction_barrier)
3. Optionally implement add/remove operations (MutableNetworkABC)
"""
# Subclasses must define these
_species_dict: dict[str, Species]
_reactions_dict: dict[int, Reaction]
# ========================================================================
# Properties (NetworkABC Implementation)
# ========================================================================
@property
[docs]
def species(self) -> dict[str, Species]:
"""Get species dictionary.
Returns
-------
dict[str, Species]
Ordered dict of species in the network, keyed by name.
"""
return self._species_dict
@property
[docs]
def reactions(self) -> dict[int, Reaction]:
"""Get reactions dictionary.
Returns
-------
dict[int, Reaction]
Ordered dict of reactions in the network, keyed by index.
"""
return self._reactions_dict
# ========================================================================
# Species Read Interface (NetworkABC Implementation)
# ========================================================================
[docs]
def get_species_list(self) -> list[Species]:
"""Get all species as a list.
Returns
-------
list[Species]
list of all species in the Network.
"""
return list(self._species_dict.values())
[docs]
def get_species_dict(self) -> dict[str, Species]:
"""Get species dictionary (copy).
Returns
-------
dict[str, Species]
copy of species dictionary, with keys of the names
of the species, and values their Species instances.
"""
return deepcopy(self._species_dict)
[docs]
def get_specie(self, specie_name: str) -> Species:
"""Get a species by name (copy).
Parameters
----------
specie_name : str
species name
Returns
-------
Species
copy of Species instance.
"""
return deepcopy(self._species_dict[specie_name])
# ========================================================================
# Reaction Read Interface (NetworkABC Implementation)
# ========================================================================
[docs]
def get_reaction_list(self) -> list[Reaction]:
"""Get all reactions as a list.
Returns
-------
list[Reaction]
list of all reactions in the Network.
"""
return list(self._reactions_dict.values())
[docs]
def get_reaction_dict(self) -> dict[int, Reaction]:
"""Get reactions dictionary (copy).
Returns
-------
dict[int, Reaction]
copy of reaction dictionary, with keys of the
indices and values of the reactions.
"""
return deepcopy(self._reactions_dict)
[docs]
def get_reaction(self, reaction_idx: int) -> Reaction:
"""Get a reaction by index (copy).
Parameters
----------
reaction_idx : int
Index of the reaction
Returns
-------
Reaction
copy of reaction with index reaction_idx.
"""
return deepcopy(self._reactions_dict[reaction_idx])
# ========================================================================
# Query Methods (NetworkABC Implementation)
# ========================================================================
[docs]
def get_reactions_by_types(self, reaction_type: str | list[str]) -> list[Reaction]:
"""Get all reactions of specific type(s).
Parameters
----------
reaction_type : str | list[str]
Single type or list of types to filter by
Returns
-------
list[Reaction]
List of reactions matching the type(s)
"""
if isinstance(reaction_type, str):
reaction_type = [reaction_type]
return [
reaction
for reaction in self._reactions_dict.values()
if reaction.get_reaction_type() in reaction_type
]
[docs]
def find_similar_reactions(self, reaction: Reaction) -> dict[int, Reaction]:
"""Find reactions with same reactants and products.
Parameters
----------
reaction : Reaction
Reaction to find similar reactions for
Returns
-------
dict[int, Reaction]
Dictionary of {index: Reaction} for matching reactions
"""
similar = {}
target_reactants = set(reaction.get_reactants()) - {"NAN"}
target_products = set(reaction.get_products()) - {"NAN"}
for idx, other_reaction in self._reactions_dict.items():
other_reactants = set(other_reaction.get_reactants()) - {"NAN"}
other_products = set(other_reaction.get_products()) - {"NAN"}
if other_reactants == target_reactants and other_products == target_products:
similar[idx] = other_reaction
return similar
[docs]
def get_reaction_index(self, reaction: Reaction) -> int:
"""Get the index of a reaction in the network.
Parameters
----------
reaction : Reaction
Reaction to find
Returns
-------
int
Index of the reaction
Raises
------
ValueError
If reaction not found or multiple matches exist
"""
similar = self.find_similar_reactions(reaction)
if len(similar) == 0:
msg = f"Reaction {reaction} not found in network"
raise ValueError(msg)
elif len(similar) > 1:
msg = (
f"Multiple reactions match {reaction}. "
f"Found indices: {list(similar.keys())}"
)
raise ValueError(msg)
return list(similar.keys())[0]
# ============================================================================
# Network Class
# ============================================================================
[docs]
class Network(BaseNetwork, MutableNetworkABC):
"""Universal network representation for build and analysis contexts.
A single Network class that serves all use cases:
- Build time: Full validation and automatic reaction generation
- Analysis time: Fast loading of compiled networks from CSV
The Network class can be created via:
- Direct instantiation: Network(species_dict, reaction_dict)
- Factory methods: from_csv(), from_lists(), build()
- Factory functions: load_network_from_csv(), build_network(), etc.
All creation methods produce a Network instance that implements the
full NetworkABC interface, ensuring consistent access patterns.
For runtime parameter modification during model execution, use RuntimeNetwork
from uclchem.advanced.runtime_network instead.
Attributes
----------
_species_dict : dict[str, Species]
Internal species storage {name: Species}
_reactions_dict : dict[int, Reaction]
Internal reaction storage {index: Reaction}
Examples
--------
>>> # Load for analysis
>>> network = Network.from_csv()
>>> # Build with validation
>>> from uclchem.makerates.io_functions import read_species_file, read_reaction_file
>>> from uclchem.utils import UCLCHEM_ROOT_DIR
>>>
>>> species_list, user_defined_bulk = read_species_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_species.csv"
... )
>>> reactions_list, dropped_reactions = read_reaction_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_grain_network.csv",
... species_list,
... "UCL",
... )
>>> network = Network.build( # doctest: +SKIP
... species_list, reactions_list, gas_phase_extrapolation=True
... )
"""
def __init__(
self, species_dict: dict[str, Species], reaction_dict: dict[int, Reaction]
):
"""Initialize network with species and reactions.
This is the low-level constructor. Most users should prefer factory methods:
- Network.from_csv() for analysis
- Network.from_lists() for direct instantiation
- Network.build() for full build with validation
Or use the module-level factory functions for clearer documentation.
For runtime parameter modification, use RuntimeNetwork from
uclchem.advanced.runtime_network instead.
Parameters
----------
species_dict : dict[str, Species]
Species dictionary {name: Species}
reaction_dict : dict[int, Reaction]
Reaction dictionary {index: Reaction}
"""
[docs]
self._species_dict = species_dict
[docs]
self._reactions_dict = reaction_dict
# Attributes set during the build phase (NetworkBuilder).
# Declared here so mypy knows about them.
[docs]
self.user_defined_bulk: list = []
[docs]
self.add_crp_photo_to_grain: bool = False
[docs]
self.derive_reaction_exothermicity: list[str] | None = None
[docs]
self.database_reaction_exothermicity: list[str | Path] | None = None
[docs]
self.enthalpies_present: bool = False
[docs]
self.excited_species: bool = False
# Populated by NetworkBuilder._index_important_reactions()
[docs]
self.important_reactions: dict[str, int | None] = {}
# Populated by NetworkBuilder._index_important_species()
[docs]
self.species_indices: dict[str, int] = {}
# ========================================================================
# Factory Methods (Class Methods)
# ========================================================================
@classmethod
[docs]
def from_csv(
cls,
species_path: str | bytes | Path | None = None,
reactions_path: str | bytes | Path | None = None,
) -> "Network":
"""Load network from CSV files.
Loads a pre-compiled network from CSV files without any validation
or automatic generation. This is the primary method for loading
networks for analysis purposes.
Parameters
----------
species_path : str | bytes | Path | None
Path to species CSV (None = use default installation)
reactions_path : str | bytes | Path | None
Path to reactions CSV (None = use default installation)
Returns
-------
Network
Loaded network instance
Examples
--------
>>> # Load default compiled network
>>> network = Network.from_csv()
>>> # Load old/custom network for analysis
>>> network = Network.from_csv('old/species.csv', 'old/reactions.csv') # doctest: +SKIP
"""
# Use defaults if not provided
if species_path is None:
species_path = UCLCHEM_ROOT_DIR / "species.csv"
if reactions_path is None:
reactions_path = UCLCHEM_ROOT_DIR / "reactions.csv"
# Decode bytes to str so pd.read_csv receives a supported type
if isinstance(species_path, bytes):
species_path = species_path.decode()
if isinstance(reactions_path, bytes):
reactions_path = reactions_path.decode()
logger.debug(f"Loading network from {species_path} and {reactions_path}")
# Load CSVs
species_data = get_species_table(species_path)
reactions_data = get_reaction_table(reactions_path)
# Parse into objects
species_list = [Species(list(spec)) for idx, spec in species_data.iterrows()]
reactions_list = [Reaction(list(reac)) for idx, reac in reactions_data.iterrows()]
# Create dictionaries
species_dict = {s.get_name(): s for s in species_list}
reaction_dict = dict(enumerate(reactions_list))
return cls(species_dict, reaction_dict)
@classmethod
[docs]
def from_lists(
cls,
species: list[Species],
reactions: list[Reaction],
) -> "Network":
"""Create network directly from lists.
Direct instantiation from species and reaction lists without any
validation or automatic generation. Useful for programmatic network
construction or as a base for NetworkBuilder.
Parameters
----------
species : list[Species]
List of Species objects
reactions : list[Reaction]
List of Reaction objects
Returns
-------
Network
Network instance
Examples
--------
>>> from uclchem.makerates.io_functions import read_species_file, read_reaction_file
>>> from uclchem.utils import UCLCHEM_ROOT_DIR
>>>
>>> species_list, user_defined_bulk = read_species_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_species.csv"
... )
>>> reactions_list, dropped_reactions = read_reaction_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_grain_network.csv",
... species_list,
... "UCL",
... )
>>> network = Network.from_lists(species_list, reactions_list) # doctest: +SKIP
"""
species_dict = {s.get_name(): s for s in species}
reaction_dict = dict(enumerate(reactions))
return cls(species_dict, reaction_dict)
@classmethod
[docs]
def build(
cls, species: list[Species], reactions: list[Reaction], **build_options
) -> "Network":
"""Build network with full validation and automatic generation.
This is the primary method for building new networks with full validation,
automatic reaction generation (freeze-out, desorption, bulk), branching
ratio checks, and all build-time operations. Delegates to NetworkBuilder.
Parameters
----------
species : list[Species]
List of Species objects
reactions : list[Reaction]
List of Reaction objects
**build_options : dict
Options passed to NetworkBuilder:
- user_defined_bulk: List of user-defined bulk species
- gas_phase_extrapolation: bool (default False)
- add_crp_photo_to_grain: bool (default False)
- derive_reaction_exothermicity: list[str] or None
- database_reaction_exothermicity: list[Union[str, Path]] or None
Returns
-------
Network
Fully built and validated network
Examples
--------
>>> from uclchem.makerates.io_functions import read_species_file, read_reaction_file
>>> from uclchem.utils import UCLCHEM_ROOT_DIR
>>>
>>> species_list, user_defined_bulk = read_species_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_species.csv"
... )
>>> reactions_list, dropped_reactions = read_reaction_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_grain_network.csv",
... species_list,
... "UCL",
... )
>>> network = Network.build( # doctest: +SKIP
... species=species_list,
... reactions=reactions_list,
... gas_phase_extrapolation=True,
... add_crp_photo_to_grain=True
... )
"""
from uclchem.makerates.network_builder import NetworkBuilder # noqa: PLC0415
builder = NetworkBuilder(species, reactions, **build_options)
return builder.build()
# ========================================================================
# Properties
# ========================================================================
@property
[docs]
def species(self) -> dict[str, Species]:
"""Get species dictionary.
Returns
-------
dict[str, Species]
Ordered dict of species in the network, keyed by name.
"""
return self._species_dict
# Note: Read operations (get_species_list, get_reaction_list, etc.)
# are inherited from BaseNetwork
# ========================================================================
# Species Mutation Interface (MutableNetworkABC Implementation)
# ========================================================================
[docs]
def set_specie(self, species_name: str, species: Species) -> None:
"""Set/update a species.
Parameters
----------
species_name : str
Name of the species.
species : Species
Species instance or list of species to add.
"""
self._species_dict[species_name] = species
[docs]
def set_species_dict(self, new_species_dict: dict[str, Species]) -> None:
"""Replace entire species dictionary.
Parameters
----------
new_species_dict : dict[str, Species]
Replacement species dictionary.
"""
self._species_dict = new_species_dict
[docs]
def add_species(self, species: Species | list) -> None:
"""Add species to network.
Parameters
----------
species : Species | list
Species object, list of Species, or CSV-style entries
Raises
------
ValueError
If there is an error when converting the CSV-style entries to
Species instances
TypeError
If input was not a Species, list of Species instances, or
CSV-style entries.
"""
# Convert to list of Species objects
species_list: list[Species]
if isinstance(species, list):
if len(species) == 0:
logger.warning("Tried to add empty species list, ignoring.")
return
elif isinstance(species[0], Species):
species_list = species # type: ignore[assignment]
elif isinstance(species[0], list):
try:
species_list = [Species(spec) for spec in species]
except ValueError as error:
msg = "Failed to convert CSV entries to Species objects"
raise ValueError(msg) from error
else:
msg = "Input must be Species object, list of Species, or CSV entries"
raise TypeError(msg)
elif isinstance(species, Species):
species_list = [species]
else:
msg = "Input must be Species object, list of Species, or CSV entries"
raise TypeError(msg)
# Add to dictionary
for specie in species_list:
# Filter out reaction types
if specie.get_name() in REACTION_TYPES:
logger.info(f"Ignoring reaction type {specie.get_name()} in species list")
continue
# Warn on duplicates
if specie.get_name() in self._species_dict:
logger.warning(
f"Species {specie.get_name()} already exists, keeping old definition"
)
continue
# Filter out empty species
if specie.get_name() in {"", "NAN"}:
continue
self._species_dict[specie.get_name()] = specie
[docs]
def remove_species(self, specie_name: str) -> None:
"""Remove a species from network.
Parameters
----------
specie_name : str
Name of the species.
"""
if specie_name in self._species_dict:
del self._species_dict[specie_name]
else:
logger.warning(f"Species {specie_name} not found in network")
[docs]
def sort_species(self) -> None:
"""Sort species by type and mass, with electron last."""
species_dict = self.get_species_dict()
self.set_species_dict(
dict(
sorted(
species_dict.items(),
key=lambda kv: (
kv[1].is_ice_species(),
kv[1].is_bulk_species(),
kv[1].get_mass(),
),
)
)
)
# Move electron to end if present
if "E-" in self._species_dict:
electron = self.get_specie("E-")
self.remove_species("E-")
self.add_species(electron)
# ========================================================================
# Reaction Mutation Interface (MutableNetworkABC Implementation)
# ========================================================================
[docs]
def set_reaction(self, reaction_idx: int, reaction: Reaction) -> None:
"""Set/update a reaction at specific index.
Parameters
----------
reaction_idx : int
Index of the reaction in the network.
reaction : Reaction
Reaction instance to look up or modify.
Raises
------
AssertionError
If setting the reaction changes the total count of reactions.
"""
old_length = len(self._reactions_dict)
self._reactions_dict[reaction_idx] = reaction
if old_length != len(self._reactions_dict):
msg = "Setting the reaction caused a change in the number of reactions"
raise AssertionError(msg)
[docs]
def set_reaction_dict(self, new_dict: dict[int, Reaction]) -> None:
"""Replace entire reaction dictionary.
Parameters
----------
new_dict : dict[int, Reaction]
Replacement reactions dictionary.
"""
self._reactions_dict = new_dict
[docs]
def add_reactions(self, reactions: Reaction | list) -> None:
"""Add reactions to network.
Parameters
----------
reactions : Reaction | list
Reaction object, list of Reactions,
or CSV-style entries
Raises
------
ValueError
If there is an error when converting the CSV-style entries to
Reaction instances
TypeError
If input was not a Reaction, list of Reaction instances, or
CSV-style entries.
"""
# Convert to list of Reaction objects
reactions_list: list[Reaction]
if isinstance(reactions, list):
if len(reactions) == 0:
logger.warning("Tried to add empty reactions list, ignoring.")
return
elif isinstance(reactions[0], Reaction):
reactions_list = reactions # type: ignore[assignment]
elif isinstance(reactions[0], list):
try:
reactions_list = [Reaction(reac) for reac in reactions]
except ValueError as error:
msg = "Failed to convert CSV entries to Reaction objects"
raise ValueError(msg) from error
else:
msg = "Input must be Reaction object, list of Reactions, or CSV entries"
raise TypeError(msg)
elif isinstance(reactions, Reaction):
reactions_list = [reactions]
else:
msg = "Input must be Reaction object, list of Reactions, or CSV entries"
raise TypeError(msg)
# Add to dictionary
for reaction in reactions_list:
if len(self._reactions_dict) == 0:
new_idx = 0
else:
new_idx = max(self._reactions_dict.keys()) + 1
self._reactions_dict[new_idx] = reaction
[docs]
def remove_reaction(self, reaction: Reaction) -> None:
"""Remove a reaction from network.
Parameters
----------
reaction : Reaction
Reaction to remove from the network.
Raises
------
RuntimeError
If multiple matching reactions are found in the network.
"""
similar_reactions = list(self.find_similar_reactions(reaction).items())
if len(similar_reactions) == 1:
reaction_idx, _ = similar_reactions[0]
del self._reactions_dict[reaction_idx]
elif len(similar_reactions) == 0:
logger.warning(f"Reaction {reaction} not found in network")
else:
msg = (
f"Found {len(similar_reactions)} reactions matching {reaction}. "
"Use remove_reaction_by_index for piecewise reactions."
)
raise RuntimeError(msg)
for coupled_reaction in self.get_all_partners(reaction):
reaction_idx = self.get_reaction_index(coupled_reaction)
del self._reactions_dict[reaction_idx]
[docs]
def remove_reaction_by_index(self, reaction_idx: int) -> None:
"""Remove a reaction by its index.
Parameters
----------
reaction_idx : int
Index of the reaction in the network.
"""
if reaction_idx in self._reactions_dict:
del self._reactions_dict[reaction_idx]
else:
logger.warning(f"Reaction index {reaction_idx} not found in network")
[docs]
def get_reactions_by_types(self, reaction_type: str | list[str]) -> list[Reaction]:
"""Get the union of all reactions of a certain type.
Parameters
----------
reaction_type : str | list[str]
The reaction type to filter on
Returns
-------
list[Reaction]
A list of reactions of the specified type
"""
if isinstance(reaction_type, str):
reaction_type = [reaction_type]
return [
r
for r in self.get_reaction_list()
if (r.get_reaction_type() in reaction_type)
]
[docs]
def sort_reactions(self) -> None:
"""Sort reactions by type and first reactant.
Raises
------
AssertionError
If sorting changes the total count of reactions.
"""
reaction_dict = self.get_reaction_dict()
self.set_reaction_dict(
dict(
sorted(
reaction_dict.items(),
key=lambda kv: (
kv[1].get_reaction_type(),
kv[1].get_reactants()[0],
),
)
)
)
if len(reaction_dict) != len(self.get_reaction_dict()):
msg = "Sorting the species caused a difference in the number of species"
raise AssertionError(msg)
# Note: Query methods (find_similar_reactions, get_reaction_index, etc.)
# are inherited from BaseNetwork
# ========================================================================
# Parameter Modification Methods (NetworkABC Implementation)
# ========================================================================
[docs]
def change_binding_energy(self, specie: str, new_binding_energy: float) -> None:
"""Change binding energy of a species.
Handles special case of @H2O which affects other bulk species.
Parameters
----------
specie : str
string representation of species
new_binding_energy : float
new binding energy in K
Raises
------
ValueError
If `specie` is not in the network.
"""
all_species = self.get_species_list()
all_species_names = [s.get_name() for s in all_species]
if specie not in all_species_names:
msg = f"Species {specie} not found in network"
raise ValueError(msg)
# Special handling for @H2O (affects all bulk species)
if specie == "@H2O":
old_h2o_be = self._species_dict["@H2O"].get_binding_energy()
for species_obj in all_species:
if (
"@" in species_obj.get_name()
and species_obj.get_binding_energy() == old_h2o_be
):
species_obj.set_binding_energy(new_binding_energy)
else:
# Warn if changing bulk species that was H2O-limited
if "@" in specie and "@H2O" in self._species_dict:
h2o_be = self._species_dict["@H2O"].get_binding_energy()
if self._species_dict[specie].get_binding_energy() == h2o_be:
logger.warning(
f"Changing binding energy of bulk species {specie} "
"that was previously @H2O binding energy limited"
)
self._species_dict[specie].set_binding_energy(new_binding_energy)
[docs]
def change_reaction_barrier(self, reaction: Reaction, barrier: float) -> None:
"""Change activation barrier of a reaction.
Looks up reaction in Network by its reactants and products.
If Fortran interface is available, also updates Fortran.
Parameters
----------
reaction : Reaction
Reaction to change.
barrier : float
New reaction barrier in K
Raises
------
RuntimeError
If multiple matching reactions are found in the network.
"""
similar_reactions = list(self.find_similar_reactions(reaction).items())
if len(similar_reactions) == 1:
reaction_idx, _ = similar_reactions[0]
self._reactions_dict[reaction_idx].set_gamma(barrier)
elif len(similar_reactions) == 0:
logger.warning(f"Reaction {reaction} not found in network")
else:
msg = (
f"Found {len(similar_reactions)} reactions matching {reaction}. "
"Cannot uniquely identify which barrier to change."
)
raise RuntimeError(msg)
[docs]
def get_all_partners(self, reaction: Reaction) -> list[Reaction]:
"""Get a list of all reactions that have ``reaction`` as their partner.
Parameters
----------
reaction : Reaction
Reaction
Returns
-------
reactions_coupled_to_reaction : list[Reaction]
List of
reactions that have ``reaction`` as their partner.
Raises
------
RuntimeError
If the partner of a :class:`CoupledReaction` instance
in the network is None.
"""
reactions_coupled_to_reaction: list[Reaction] = []
for possible_partner_reaction in self.get_reaction_list():
if possible_partner_reaction == reaction:
continue
if not isinstance(possible_partner_reaction, CoupledReaction):
continue
partner = possible_partner_reaction.get_partner()
if partner is None:
msg = f"The partner of {possible_partner_reaction} was None"
raise RuntimeError(msg)
if partner == reaction:
reactions_coupled_to_reaction.append(possible_partner_reaction)
return reactions_coupled_to_reaction
# ============================================================================
# Factory Functions (Module-Level)
# ============================================================================
[docs]
def load_network_from_csv(
species_path: str | Path | None = None,
reactions_path: str | Path | None = None,
) -> Network:
"""Load a network from CSV files for analysis.
This is a module-level factory function that provides clear documentation
and intent. It calls Network.from_csv() internally.
Use this when analyzing pre-compiled networks, comparing network versions,
or loading old networks for analysis.
Parameters
----------
species_path : str | Path | None
Path to species CSV (None = use default installation)
reactions_path : str | Path | None
Path to reactions CSV (None = use default installation)
Returns
-------
Network
Loaded network instance
Examples
--------
>>> # Load default cmpiled network
>>> network = load_network_from_csv()
>>> # Load old version for comparison
>>> old_network = load_network_from_csv(
... 'archive/v3.0/species.csv',
... 'archive/v3.0/reactions.csv'
... ) # doctest: +SKIP
>>> print(f"Species added: {len(network.get_species_list()) - len(old_network.get_species_list())}") # doctest: +SKIP
""" # noqa: W505
return Network.from_csv(species_path, reactions_path)
[docs]
def build_network(
species: list[Species],
reactions: list[Reaction],
user_defined_bulk: list | None = None,
gas_phase_extrapolation: bool = False,
add_crp_photo_to_grain: bool = False,
derive_reaction_exothermicity: list[str] | None = None,
database_reaction_exothermicity: list[str | Path] | None = None,
) -> Network:
"""Build a new network with full validation and automatic generation.
This is a module-level factory function that provides clear documentation
and intent. It calls Network.build() internally.
Use this when creating new networks at build time. This performs:
- Input validation and duplicate checking
- Automatic freeze-out reaction generation
- Automatic bulk species and reaction generation
- Automatic desorption reaction generation
- Branching ratio validation and correction
- Temperature range collision detection
- Optional gas-phase extrapolation
- Optional reaction exothermicity calculation
Parameters
----------
species : list[Species]
List of Species objects
reactions : list[Reaction]
List of Reaction objects
user_defined_bulk : list | None
User-specified bulk species (optional). Defaults to ``None``.
gas_phase_extrapolation : bool
Extrapolate gas-phase reactions temperatures. Defaults to ``False``.
add_crp_photo_to_grain : bool
Add CRP/PHOTON reactions to grain surface. Defaults to ``False``.
derive_reaction_exothermicity : list[str] | None
Reaction types to calculate exothermicity for. Defaults to ``None``.
database_reaction_exothermicity : list[str | Path] | None
Custom exothermicity database files. Defaults to ``None``.
Returns
-------
Network
Fully built and validated network
Examples
--------
>>> from uclchem.makerates.io_functions import read_species_file, read_reaction_file
>>> from uclchem.utils import UCLCHEM_ROOT_DIR
>>>
>>> species_list, user_defined_bulk = read_species_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_species.csv"
... )
>>> reactions_list, dropped_reactions = read_reaction_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_grain_network.csv",
... species_list,
... "UCL",
... )
>>> # Build network with standard options
>>> network = build_network( # doctest: +SKIP
... species=species_list,
... reactions=reactions_list,
... gas_phase_extrapolation=True
... )
>>> # Build with custom exothermicity
>>> network = build_network(
... species=species_list,
... reactions=reactions_list,
... derive_reaction_exothermicity=['PHOTON', 'CRP'],
... database_reaction_exothermicity=['custom_heating.csv']
... ) # doctest: +SKIP
"""
return Network.build(
species=species,
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,
)
[docs]
def create_network(
species: list[Species],
reactions: list[Reaction],
) -> Network:
"""Create a network directly from species and reaction lists.
This is a module-level factory function that provides clear documentation
and intent. It calls Network.from_lists() internally.
Use this when you want direct instantiation without validation or
automatic generation. Suitable for programmatic network construction
or when you already have a fully prepared network.
Parameters
----------
species : list[Species]
List of Species objects
reactions : list[Reaction]
List of Reaction objects
Returns
-------
Network
Network instance
Examples
--------
>>> from uclchem.makerates.io_functions import read_species_file, read_reaction_file
>>> from uclchem.utils import UCLCHEM_ROOT_DIR
>>>
>>> species_list, user_defined_bulk = read_species_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_species.csv"
... )
>>> reactions_list, dropped_reactions = read_reaction_file( # doctest: +SKIP
... UCLCHEM_ROOT_DIR / "../../Makerates/data/default/default_grain_network.csv",
... species_list,
... "UCL",
... )
>>>
>>> network = create_network(species_list, reactions_list) # doctest: +SKIP
>>>
>>> # Add some additional reactions
>>> network.add_reactions(additional_reactions) # doctest: +SKIP
"""
return Network.from_lists(species, reactions)