Source code for rnets.plotter.kinetic

"""Kinetic plot module. Given the energies and concentrations of the compounds
and the energies transition states, this module creates a graph summarizing the
kinetic behavior of the system. The background of the nodes is colored
depending on their concentration, while the width of the edges and the
direction of the arrows is decided based on the calculated net rate.
"""
from collections.abc import Sequence
from itertools import chain, repeat, starmap
from functools import partial, reduce
from typing import Callable, Iterator, Iterable

from ..colors.utils import Color, ColorSpace, interp_cs
from ..chemistry import (
    calc_net_rate
    , normalizer
    , minmax
    , ChemCfg
    , network_conc_normalizer
)
from ..addons.colorbar import build_colorbar, build_anchor, ColorbarCfg
from ..dot import Edge, Graph, Node
from ..struct import Reaction, Network, Visibility

from .utils import (
    EdgeArgs
    , build_glob_opt
    , build_dotnode
    , build_dotedges
    , nodecolor_sel
    , color_interp
    , GraphCfg
)

[docs] def get_colorbar( nw: Network , graph_cfg: GraphCfg , colorbar_cfg: ColorbarCfg , colorspace: ColorSpace="lab" ) -> tuple[Node, Edge | tuple[()]]: """Build a colorbar for the thermodynamic plot. Args: graph_cfg (:obj:`GraphCfg`, optional): Graphviz configuration. Defaults to :obj:`ChemCfg`. colorbar_cfg (:obj:`ColorbarCfg` or None, optional): Colorbar parameters of the system. Defaults to None. colorspace (ColorSpace, optional): Colorspace of the colorbar. Defaults to "lab". Returns: tuple of two values, the first one being a obj:`Node` representing the color bar and the second value being an invisible edge that anchors the colorbar to another node. """ def f_none[T](xs: Iterable[T | None]) -> Iterable[T]: return (x for x in xs if x is not None) c_range = minmax(starmap( getattr , zip(f_none(nw.compounds), repeat("conc")))) anchor: Edge | tuple[()] = () if colorbar_cfg.anchor is not None: anchor = build_anchor(colorbar_cfg.anchor, colorbar_cfg.node_name) return ( build_colorbar( interp_cs(graph_cfg.colorscheme, colorspace) , c_range , colorbar_cfg , "Concentration") , anchor)
[docs] def filter_unique_react( rs: Sequence[Reaction] ) -> tuple[Reaction, ...]: """Given a set of reactions that may contain biderectional reactions, return a set without the reversed reactions. Args: rs (sequence of :obj:`Reaction`): Sequence of reactions that will bee filtered. Returns: Tuple containing the unique :obj:`Reaction` s. """ def r_fn(xs: tuple[Reaction, ...], x: Reaction) -> tuple[Reaction, ...]: cmp_hash: int = hash(tuple(reversed(x.compounds))) fn_check = lambda y: hash(y.compounds) == cmp_hash return xs if any(map(fn_check, xs)) else xs + (x,) return reduce(r_fn, rs, ())
[docs] def build_dotgraph( nw: Network , graph_cfg: GraphCfg = GraphCfg() , chem_cfg: ChemCfg = ChemCfg() , colorbar_cfg: ColorbarCfg | None = None ) -> Graph: """Build a kinetic dotgraph from a reaction network. Args: nw (:obj:`Network`): Network object to be converted into dot graph. graph_cfg (:obj:`GraphCfg`, optional): Graphviz configuration. Defaults to :obj:`ChemCfg` chem_cfg (:obj:`ChemCfg`, optional): Chemical parameters fo the system. Defaults to :obj:`ChemCfg` colorbar_cfg (:obj:`ColorbarCfg` or None, optional): Colorbar parameters of the system. Defaults to None. Returns: Dot :obj:`Graph` representing the kinetic information with the colors and shapes of the network. """ if colorbar_cfg is None: cb_node, cb_edge = ((),()) else: cb_node, cb_edge = get_colorbar( nw , graph_cfg , colorbar_cfg) c_norm: Callable[[float], Color] = color_interp( norm_fn=network_conc_normalizer(nw) , cs=graph_cfg.colorscheme , offset=graph_cfg.color_offset ) n_color_fn: Callable[[float, Visibility], tuple [Color, Color]] = nodecolor_sel( c_norm=c_norm , fg_c=graph_cfg.node.font_color , fg_alt=graph_cfg.node.font_color_alt , lum_threshold=graph_cfg.node.font_lum_threshold ) u_react: tuple[Reaction, ...] = filter_unique_react(nw.reactions) e_colors: Iterator[Color | None] = repeat(graph_cfg.edge.solid_color) e_widths: Iterator[float] e_dir: Iterator[bool] if graph_cfg.edge.max_width is None: e_widths = repeat(graph_cfg.edge.width) # TODO: Draw it with lines e_dir = repeat(False) else: rates: tuple[float, ...] = tuple(map( partial(calc_net_rate, T=chem_cfg.T, A=chem_cfg.A, kb=chem_cfg.kb) , u_react)) def e_width_aux(x: float) -> float: assert graph_cfg.edge.max_width is not None return x * (graph_cfg.edge.max_width - graph_cfg.edge.width) + graph_cfg.edge.width e_widths: Iterator[float] = map( e_width_aux , map( normalizer(*minmax(map(abs, rates))) , rates) ) e_dir = map(lambda x: x < 0, rates) return Graph( kind=graph_cfg.kind , nodes=tuple(map( lambda c: build_dotnode(c, *n_color_fn(c.conc, c.visible)) , filter(lambda c: c.visible != Visibility.FALSE, nw.compounds) )) + ((cb_node,) if cb_node else cb_node) , edges=tuple(chain.from_iterable(starmap( build_dotedges , filter( # Tuple for __getitem__ lambda xs: EdgeArgs(*xs).react.visible != Visibility.FALSE , zip(u_react, e_widths, e_colors, e_dir) ) ))) + ((cb_edge,) if cb_edge else cb_edge) , options=build_glob_opt(graph_cfg) )