"""Implements a class for storing a balanced reaction pathway."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from rxn_network.pathways.basic import BasicPathway
from rxn_network.utils.funcs import limited_powerset
if TYPE_CHECKING:
from rxn_network.core import Composition
from rxn_network.pathways.base import Pathway
from rxn_network.reactions.base import Reaction
[docs]
class BalancedPathway(BasicPathway):
"""Helper class for storing multiple ComputedReaction objects which form a single
reaction pathway as identified via pathfinding methods. Includes costs for each
reaction.
"""
def __init__(
self,
reactions: list[Reaction],
coefficients: list[float],
costs: list[float],
balanced: bool = False,
):
"""
Args:
reactions: list of ComputedReaction objects which occur along path.
coefficients: list of coefficients to balance each corresponding reaction.
costs: list of corresponding costs for each reaction.
balanced: whether or not the reaction pathway is balanced.
Defaults to False and should ideally be set through PathwaySolver.
"""
self.coefficients = coefficients
super().__init__(reactions=reactions, costs=costs)
self.balanced = balanced
[docs]
def get_comp_matrix(self) -> np.ndarray:
"""Gets the composition matrix used in the balancing procedure.
Returns:
An array representing the composition matrix for a reaction
"""
return np.array(
[
[rxn.get_coeff(comp) if comp in rxn.all_comp else 0 for comp in self.compositions]
for rxn in self.reactions
]
)
[docs]
def get_coeff_vector_for_rxn(self, rxn: Reaction) -> np.ndarray:
"""Gets the net reaction coefficients vector.
Args:
rxn: Reaction object to get coefficients for
Returns:
An array representing the reaction coefficients vector
"""
return np.array([rxn.get_coeff(comp) if comp in rxn.compositions else 0 for comp in self.compositions])
[docs]
def contains_interdependent_rxns(self, precursors: list[Composition]) -> bool:
"""Whether or not the pathway contains interdependent reactions given a list of
provided precursors.
Args:
precursors: List of precursor compositions
"""
precursors_set = set(precursors)
interdependent = False
rxns = set(self.reactions)
num_rxns = len(rxns)
if num_rxns == 1:
return False
for combo in limited_powerset(rxns, num_rxns):
size = len(combo)
if any(set(rxn.reactants).issubset(precursors_set) for rxn in combo) or size == 1:
continue
other_comp = {c for rxn in (rxns - set(combo)) for c in rxn.compositions}
unique_reactants = []
unique_products = []
for rxn in combo:
unique_reactants.append(set(rxn.reactants) - precursors_set)
unique_products.append(set(rxn.products) - precursors_set)
overlap = [False] * size
for i in range(size):
for j in range(size):
if i == j:
continue
overlapping_phases = unique_reactants[i] & unique_products[j]
if overlapping_phases and (overlapping_phases not in other_comp):
overlap[i] = True
if all(overlap):
interdependent = True
return interdependent
[docs]
@classmethod
def balance(
cls,
pathway_sets: list[Pathway] | list[list[Reaction]],
net_reaction: Reaction,
tol: float = 1e-6,
):
"""Not implemented. See PathwaySolver class."""
_, _, _ = pathway_sets, net_reaction, tol
raise NotImplementedError(
"Currently, to automatically balance and create a BalancedPathway object,"
" you must use the PathwaySolver class."
)
@property
def average_cost(self) -> float:
"""Returns the mean cost of the pathway."""
return np.dot(self.coefficients, self.costs) / sum(self.coefficients)
def __eq__(self, other) -> bool:
if super().__eq__(other):
return np.allclose(self.costs, other.costs)
return False
def __hash__(self):
return hash((tuple(self.reactions), tuple(self.coefficients)))
def __repr__(self) -> str:
path_info = ""
for rxn in self.reactions:
path_info += f"{rxn} (dG = {round(rxn.energy_per_atom, 3)} eV/atom) \n"
path_info += f"Average Cost: {round(self.average_cost,3)}"
return path_info