Source code for fluopy.transitions

"""
Define and handle photophysical transitions.
"""

from __future__ import annotations

import copy
import re
from collections.abc import Collection, Iterable
from dataclasses import asdict, dataclass, field
from enum import Enum
from itertools import product
from pathlib import Path
from typing import TYPE_CHECKING, Self

import numpy as np
import numpy.typing as npt
import pandas as pd
from scipy import interpolate as itp

from . import formulas as fo
from . import network as net

if TYPE_CHECKING:
    from matplotlib.axes import Axes as mplAxes

    from fluopy.fluo_data import FluorophoreData
    from fluopy.fluorophores import Fluorophore, FluorophoreSystem


__all__: list[str] = ["SingleState", "PairedState", "Transition", "TransitionSet"]


[docs] class SingleState(Enum): """ Assigns a unique identifier (value) to each possible photophysical state. """ S0 = 0 S1 = 1 S2 = 2 T1 = 3 T2 = 4 B = 5 cis = 6 OFF = 7 OFF2 = 8 R = 9
[docs] class PairedState(Enum): """ Assigns a combination of SingleState to each energy transfer related paired state. E.g., the classical Förster resonance energy transfer needs one fluorophore to be in S1 and another fluorophore closeby to be in S0. After the transition, the first fluorophore will be in S0 and the other in S1. """ S1_S0 = [SingleState.S1, SingleState.S0] S0_S1 = [SingleState.S0, SingleState.S1] S1_T1 = [SingleState.S1, SingleState.T1] S1_Cis = [SingleState.S1, SingleState.cis] S0_Cis = [SingleState.S0, SingleState.cis] S1_OFF = [SingleState.S1, SingleState.OFF] S0_S0 = [SingleState.S0, SingleState.S0] S0_T2 = [SingleState.S0, SingleState.T2] S1_S1 = [SingleState.S1, SingleState.S1] S0_T1 = [SingleState.S0, SingleState.T1] S0_OFF2 = [SingleState.S0, SingleState.OFF2] S0_OFF = [SingleState.S0, SingleState.OFF] S0_B = [SingleState.S0, SingleState.B] S1_R = [SingleState.S1, SingleState.R] S0_R = [SingleState.S0, SingleState.R] @property def single_state_values(self) -> tuple[int, int]: """ Returns a tuple of SingleState values. """ return self.value[0].value, self.value[1].value @property def acceptor(self) -> SingleState: """ Returns the acceptor (second value). """ return self.value[1] @property def donor(self) -> SingleState: """ Returns the donor (first value). """ return self.value[0]
@dataclass class TransitionAttributes: """ Contains constant attributes of photophysical transitions. Attributes ---------- abbreviation : str Abbreviation of the transition. initial_state : SingleState, PairedState Initial state of the transition. final_state : SingleState, PairedState Final state of the transition. photon : bool Whether the transition emits a photon. """ abbreviation: str initial_state: SingleState | PairedState final_state: SingleState | PairedState photon: bool class TransitionType(Enum): """ Assigns constant attributes to each possible photophysical transition. """ # general EXCITATION = TransitionAttributes("EXC", SingleState.S0, SingleState.S1, False) FLUORESCENT_EMISSION = TransitionAttributes( "FLU", SingleState.S1, SingleState.S0, True ) SINGLET_QUENCHING = TransitionAttributes( "SQ", SingleState.S1, SingleState.S0, False ) INTERSYSTEM_CROSSING_ST = TransitionAttributes( "ISC_ST", SingleState.S1, SingleState.T1, False ) INTERSYSTEM_CROSSING_TS = TransitionAttributes( "ISC_TS", SingleState.T1, SingleState.S0, False ) INTERNAL_CONVERSION_S = TransitionAttributes( "IC", SingleState.S1, SingleState.S0, False ) REVERSE_INTERSYSTEM_CROSSING = TransitionAttributes( "RISC", SingleState.T2, SingleState.S1, False ) PHOTOBLEACHING_1 = TransitionAttributes("BLE", SingleState.T1, SingleState.B, False) PHOTOBLEACHING_2 = TransitionAttributes( "BLE2", SingleState.T2, SingleState.B, False ) # dstorm ET_CYCLE_T = TransitionAttributes("PET_TS", SingleState.T1, SingleState.S0, False) ET_CYCLE_S = TransitionAttributes("PET_SS", SingleState.S1, SingleState.S0, False) ADDUCT_T = TransitionAttributes("PET_TO", SingleState.T1, SingleState.OFF, False) ADDUCT_S = TransitionAttributes("PET_SO", SingleState.S1, SingleState.OFF, False) THERM_ELIMINATION = TransitionAttributes( "TE", SingleState.OFF, SingleState.S0, False ) PHOTO_UNCAGING = TransitionAttributes("PU", SingleState.OFF, SingleState.S0, False) RAD_ESCAPE = TransitionAttributes("PET_TR", SingleState.T1, SingleState.R, False) RAD_RELAX = TransitionAttributes("OXI", SingleState.R, SingleState.S0, False) # cis trans isomerization ISOMERIZATION = TransitionAttributes("ISO", SingleState.S1, SingleState.cis, False) PHOTO_BISO = TransitionAttributes("PBISO", SingleState.cis, SingleState.S0, False) THERM_BISO = TransitionAttributes("TBISO", SingleState.cis, SingleState.S0, False) # energy transfers FRET = TransitionAttributes("FRET", PairedState.S1_S0, PairedState.S0_S1, False) CIS_FRET_1 = TransitionAttributes( "CET_1", PairedState.S1_Cis, PairedState.S0_Cis, False ) CIS_FRET_2 = TransitionAttributes( "CET_2", PairedState.S1_Cis, PairedState.S0_S0, False ) OFF_FRET_1 = TransitionAttributes( "OET_1", PairedState.S1_OFF, PairedState.S0_OFF, False ) OFF_FRET_2 = TransitionAttributes( "OET_2", PairedState.S1_OFF, PairedState.S0_S0, False ) S_S_ANNIHILATION = TransitionAttributes( "SSA", PairedState.S1_S1, PairedState.S0_S1, False ) S_T_ANNIHILATION = TransitionAttributes( "STA", PairedState.S1_T1, PairedState.S0_T1, False ) S_T_ANNI_RISC = TransitionAttributes( "STA_2", PairedState.S1_T1, PairedState.S0_S1, False ) S_T_ANNI_BLEACH = TransitionAttributes( "STA_B", PairedState.S1_T1, PairedState.S0_B, False ) R_FRET_1 = TransitionAttributes("RET_1", PairedState.S1_R, PairedState.S0_R, False) R_FRET_2 = TransitionAttributes("RET_2", PairedState.S1_R, PairedState.S0_S0, False) # rhodamines H2O_ATTACK_S = TransitionAttributes("H2OS", SingleState.S1, SingleState.OFF, False) H2O_ATTACK_T = TransitionAttributes("H2OT", SingleState.T1, SingleState.OFF, False) BACK_REACTION = TransitionAttributes("BR", SingleState.OFF, SingleState.S0, False) # summarize S1_S0_TRANSITIONS = TransitionAttributes( "S1S0SUM", SingleState.S1, SingleState.S0, False ) CIS_S0_TRANSITIONS = TransitionAttributes( "cisS0SUM", SingleState.cis, SingleState.S0, False ) T1_S0_TRANSITIONS = TransitionAttributes( "T1S0SUM", SingleState.T1, SingleState.S0, False ) OFF_S0_TRANSITIONS = TransitionAttributes( "OFFS0SUM", SingleState.OFF, SingleState.S0, False ) @property def abbreviation(self) -> str: """ Returns the abbreviation of type str. """ return self.value.abbreviation @property def initial_state(self) -> SingleState | PairedState: """ Returns the initial state of type SingleState or PairedState. """ return self.value.initial_state @property def final_state(self) -> SingleState | PairedState: """ Returns the final state of type SingleState or PairedState. """ return self.value.final_state @property def photon(self) -> bool: """ Returns bool indicating whether the transition emits a photon. """ return self.value.photon
[docs] @dataclass( slots=True ) # frozen=True if code will not be modified (autoreload complications otherwise) class Transition: """ Contains constant and variable attributes of photophysical transitions. Attributes ---------- identity : int The id of the transition. Not None if transition is part of a TransitionSet. transition_type : TransitionType The photophysical type of the transitions with its constant attributes. abbreviation : str The abbreviation of the transition. initial_state : SingleState | PairedState The initial state of the transition. final_state : SingleState | PairedState The final state of the transition. rate : float The rate of the transition. photon : bool Whether the transition emits a photon. fluorophore_ids : list[int] | list[tuple[int, int]] Contains the identities of relevant fluorophores. If energy transfer, tuples of fluorophore pairs, where the first is the donor and the second is the acceptor. """ identity: int = field(init=False) transition_type: TransitionType = field() abbreviation: str = field(init=False) initial_state: SingleState | PairedState = field(init=False) final_state: SingleState | PairedState = field(init=False) rate: float = field() photon: bool = field(init=False) fluorophore_ids: list[int] | list[tuple[int, int]] = field()
[docs] def __post_init__(self) -> None: # __setattr__ needed if frozen=True object.__setattr__(self, "abbreviation", self.transition_type.abbreviation) object.__setattr__(self, "initial_state", self.transition_type.initial_state) object.__setattr__(self, "final_state", self.transition_type.final_state) object.__setattr__(self, "photon", self.transition_type.photon) object.__setattr__(self, "identity", None) for fluorophore_id in self.fluorophore_ids: if isinstance(self.initial_state, PairedState): if not isinstance(fluorophore_id, tuple) or len(fluorophore_id) != 2: raise ValueError( f"{self.abbreviation} is energy transfer, " "fluorophore_ids have to be tuples of fluorophore " "pairs." ) else: if not isinstance(fluorophore_id, int): raise ValueError( f"{self.abbreviation} is not an energy transfer, " "fluorophore_ids has to be a list of ints." )
[docs] class TransitionSet: """ Collection of all relevant transitions and related attributes. Allows optional post-init-modification and (subsequent) finalization. Attributes ---------- transitions : dict[str, list[Transition]] Contains lists of transitions of type Transition with non-zero rate as values and fluorophores or fluorophore-combinations as keys. fluorophore_system : fluopy.fluorophores.FluorophoreSystem Container for attributes of multiple, interrelated fluorophores. combined_state_transitions_df : pd.DataFrame Contains realizable combined_state_transitions with their id as index and their other attributes as columns. row_sums : np.ndarray Contains the sum of each row of non-normalized transition rates, i.e., the sum of rates of all possible combined_state_transitions. single_states : dict[str, npt.NDArray[np.int64]] Contains the values of all relevant SingleStates as values. Name of fluorophores as keys. transition_df : pd.DataFrame Dataframe of all given transitions with non-zero rate containing their id as second level index and their other attributes as columns. Name of fluorophores as first level index. transition_matrix : np.ndarray Contains the normalized rate constants (i.e., point probabilities) for each possible combined_state_transition at the corresponding index pair. """ def __init__( self, transitions: dict[str, list[Transition]], fluorophore_system: FluorophoreSystem, keep_zero_rates: bool = False, ) -> None: """ Parameters ---------- transitions Contains lists of transitions of type Transition as values and fluorophores or fluorophore-combinations as keys. fluorophore_system Container for attributes of multiple, interrelated fluorophores. keep_zero_rates Whether to keep transitions with rate 0. """ self.transitions = transitions self.fluorophore_system = fluorophore_system self.transition_df = pd.DataFrame() i = 0 for fluorophore_comb, f_transitions in transitions.items(): keep_transitions = [] df_constructor = [] for transition in f_transitions: if "dist" not in fluorophore_comb and isinstance( transition.initial_state, PairedState ): raise ValueError( "energy transfers have to be defined in transitions with the " "key 'D: {name of donor}, A: {name of acceptor}, dist: " "{distance between them}'." ) if "dist" in fluorophore_comb: pattern = r"D:\s*([^,]+),\s*A:\s*([^,]+),\s*dist:\s*([\d.]+)" match = re.match(pattern=pattern, string=fluorophore_comb) d, a, dist = match.group(1), match.group(2), match.group(3) for d_t, a_t in transition.fluorophore_ids: if self.fluorophore_system.fluorophores[d_t].name != d: raise ValueError( f"{d} indicated to be at identity {d_t}, " f"{self.fluorophore_system.fluorophores[d_t].name} " "found." ) elif self.fluorophore_system.fluorophores[a_t].name != a: raise ValueError( f"{a} indicated to be at identity {a_t}, " f"{self.fluorophore_system.fluorophores[a_t].name} " "found." ) if str(self.fluorophore_system.distances[(d_t, a_t)]) != dist: raise ValueError( f"{dist} nm indicated, " f"{self.fluorophore_system.distances[(d_t, a_t)]} nm " "found." ) else: for j in transition.fluorophore_ids: if ( self.fluorophore_system.fluorophores[j].name != fluorophore_comb ): raise ValueError( f"{fluorophore_comb} indicated to be at identity {j}, " f"{self.fluorophore_system.fluorophores[j].name} found." ) if not keep_zero_rates: if transition.rate != 0: transition.identity = i i += 1 keep_transitions.append(transition) df_constructor.append(asdict(transition)) else: transition.identity = i i += 1 keep_transitions.append(transition) df_constructor.append(asdict(transition)) if keep_transitions: transitions[fluorophore_comb] = keep_transitions transition_df = pd.DataFrame(df_constructor) transition_df = transition_df.set_index("identity") transition_df = pd.concat( {fluorophore_comb: transition_df}, names=["Fluorophore"] ) self.transition_df = pd.concat([self.transition_df, transition_df]) self.single_states = get_single_states(self.transitions, self.transition_df) # also assigns whether a transition leads to a Markovian absorbing state self._combined_state_transitions_df = None self._row_sums = None self._transition_matrix = None @property def combined_state_transitions_df(self) -> pd.DataFrame: if self._combined_state_transitions_df is None: self.finalize() return self._combined_state_transitions_df @property def row_sums(self) -> npt.NDArray[np.float64]: if self._row_sums is None: self.finalize() return self._row_sums @property def transition_matrix(self) -> npt.NDArray[np.float64]: if self._transition_matrix is None: self.finalize() return self._transition_matrix
[docs] def filter_by_identity( self, remove_list: Collection = None, keep_zero_rates: bool = False ) -> TransitionSet: """ Returns another TransitionSet with transitions removed by their identity. Parameters ---------- remove_list Contains identities of type int. keep_zero_rates Whether to keep transitions with rate 0. Returns ------- filtered : TransitionSet Re-initialization of the object with the modified transition collection. """ transitions = copy.deepcopy(self.transitions) if remove_list is None: remove_list = [] filtered_transitions = {} for fluorophore, f_transitions in transitions.items(): for transition in f_transitions: if transition.identity not in remove_list: if fluorophore in filtered_transitions: filtered_transitions[fluorophore] += [transition] else: filtered_transitions[fluorophore] = [transition] filtered = TransitionSet( transitions=filtered_transitions, fluorophore_system=self.fluorophore_system, keep_zero_rates=keep_zero_rates, ) return filtered
[docs] def adjust_rates( self, change_dict: dict[int, float] = None, keep_zero_rates: bool = False ) -> TransitionSet: """ Returns another TransitionSet with transition rates modified. Parameters ---------- change_dict Contains identities of transitions as key and rates as values. keep_zero_rates Whether to keep transitions with rate 0. Returns ------- adjusted : TransitionSet Re-initialization of the object with the modified transition collection. """ transitions = copy.deepcopy(self.transitions) if change_dict is None: change_dict = {} for _, f_transitions in transitions.items(): for transition in f_transitions: if transition.identity in change_dict: transition.rate = change_dict[transition.identity] adjusted = TransitionSet( transitions=transitions, fluorophore_system=self.fluorophore_system, keep_zero_rates=keep_zero_rates, ) return adjusted
[docs] def remove_zero_rates(self) -> TransitionSet: """ Returns another TransitionSet with all transitions removed that have a rate constant of zero. Returns ------- TransitionSet Re-initialization of the object with the modified transition collection. """ transitions = copy.deepcopy(self.transitions) new_transition_set = TransitionSet( transitions=transitions, fluorophore_system=self.fluorophore_system, keep_zero_rates=False, ) return new_transition_set
[docs] def remove_absorbing_states(self, keep_zero_rates: bool = False) -> TransitionSet: """ Returns another TransitionSet that contains no Markovian absorbing states. Parameters ---------- keep_zero_rates Whether to keep transitions with rate 0. Returns ------- no_abs : TransitionSet Re-initialization of the object with the modified transition collection. """ transitions = copy.deepcopy(self.transitions) # transitions are objects # if no deep copy, the transition objects of the new TransitionSet are the # very same objects as in the old one keep_transitions = {} for fluorophore, f_transitions in transitions.items(): for transition in f_transitions: if not self.transition_df.loc[ (fluorophore, transition.identity), "absorbing" ]: if fluorophore in keep_transitions: keep_transitions[fluorophore] += [transition] else: keep_transitions[fluorophore] = [transition] no_abs = TransitionSet( transitions=keep_transitions, fluorophore_system=self.fluorophore_system, keep_zero_rates=keep_zero_rates, ) return no_abs
[docs] def remove_energy_transfers(self, keep_zero_rates: bool = False) -> TransitionSet: """ Return another TransitionSet that contains no transitions that are energy transfers. Parameters ---------- keep_zero_rates Whether to keep transitions with rate 0. Returns ------- no_ets : TransitionSet Re-initialization of the object with the modified transition collection. """ transitions = copy.deepcopy(self.transitions) keep_transitions = {} for fluorophore, f_transition in transitions.items(): if "dist" not in fluorophore: keep_transitions[fluorophore] = f_transition no_ets = TransitionSet( transitions=keep_transitions, fluorophore_system=self.fluorophore_system, keep_zero_rates=keep_zero_rates, ) return no_ets
[docs] def finalize(self) -> Self: """ Construct combined_state_transitions_df, transition_matrix and row_sums. Returns ------- self """ if self._combined_state_transitions_df is not None: return self state_combinations = get_state_combinations( single_states=self.single_states, fluorophores=self.fluorophore_system.fluorophores, ) combined_state_transitions = get_combined_state_transitions( state_combinations=state_combinations ) combined_state_transitions_with_rates = construct_transition_rate_list( transition_df=self.transition_df, combined_state_transitions=combined_state_transitions, ) self._combined_state_transitions_df = pd.DataFrame( combined_state_transitions_with_rates, columns=[ "initial_state", "final_state", "fluorophore_ids", "abbreviation", "transition_id", "rate", "photon", ], ) self._combined_state_transitions_df.index.name = "id" self._transition_matrix, self._row_sums = construct_transition_matrix( combined_state_transitions_df=self._combined_state_transitions_df ) return self
[docs] def plot( self, graph_type: str = "shell", colors: Collection | None = None, scale: float = 1, axes: Iterable[mplAxes] | None = None, ) -> list[mplAxes]: """ Plot photophysical system as network/graph. Parameters ---------- graph_type Specifies network layout. One of 'shell', 'circular', 'planar' or 'kamada'. colors Contains two colors as Hex values of type str. scale Factor to scale the figure. axes Axes elements to plot graphs on. Returns ------- list[matplotlib.axes.Axes] Axes objects with the plots. """ graphs = net.construct_state_graphs(transition_df=self.transition_df) if axes is None: axes = [None] * len(graphs) return_axes = [] try: for graph, ax in zip(graphs, axes, strict=True): ax = net.plot_graph( G=graph, graph_type=graph_type, colors=colors, scale=scale, ax=ax ) return_axes.append(ax) except ValueError as exception: raise ValueError( f"The number of axes elements must be {len(graphs)} or None" ) from exception return return_axes
def get_single_states( transitions: dict[str, Collection[Transition]], transition_df: pd.DataFrame ) -> dict[str, npt.NDArray[int]]: """ Gets the values of SingleStates that occur in non-energy transfer transitions. Also assigns whether a transition leads to a Markovian absorbing state (note that hypothetically, an energy transfer onto that state which yields another or the same state could still happen). Parameters ---------- transitions Contains transitions of type Transition with non-zero rate. transition_df Dataframe of all given transitions with non-zero rate containing their id as index and their other attributes as columns. Returns ------- single_states : dict Contains the values of all relevant SingleStates as values. Name of fluorophores as keys. """ transition_df["absorbing"] = False single_states = {} for fluorophore_comb, f_transitions in transitions.items(): if "dist" not in fluorophore_comb: single_states_ = [] for transition in f_transitions: initial_state = transition.initial_state final_state = transition.final_state if initial_state.value not in single_states_: single_states_.append(initial_state.value) if final_state.value not in single_states_: single_states_.append(final_state.value) single_states_ = np.array(single_states_) single_states[fluorophore_comb] = single_states_ single_state_df = pd.DataFrame(single_states_, columns=["single_states"]) single_state_df["absorbing"] = False for i, single_state in single_state_df["single_states"].items(): if ( single_state not in transition_df.loc[fluorophore_comb, "initial_state"] .apply(lambda x: x.value) .values ): single_state_df.at[i, "absorbing"] = True final_states = ( transition_df.loc[fluorophore_comb, "final_state"] .apply(lambda x: x.value) .values ) absorbing_states = single_state_df["single_states"][ single_state_df["absorbing"] ] if not absorbing_states.empty: indices = np.where(np.isin(final_states, absorbing_states.values))[0] index_values = transition_df.loc[fluorophore_comb].iloc[indices].index transition_df.loc[(fluorophore_comb, index_values), "absorbing"] = True return single_states def get_state_combinations( single_states: dict[str, Collection[int]], fluorophores: Collection[Fluorophore], ) -> list[tuple[int, ...]]: """ Combines all given states with each other according to the amount and order of the respective fluorophore. Cartesian product, see itertools.product(). Parameters ---------- single_states Contains the values of all relevant SingleStates as values. Name of fluorophores as keys. fluorophores Contains all given fluorophores of type Fluorophore. Returns ------- list Contains state combinations of type tuple. """ single_states_fluorophores = [ single_states[fluorophore.name] for fluorophore in fluorophores ] return list(product(*single_states_fluorophores)) def get_combined_state_transitions( state_combinations: Collection[tuple[int, ...]], ) -> list[tuple[tuple[int, ...], ...]]: """ Combines all given state_combinations with themselves 2 times. Cartesian product, see itertools.product(). Each combination resembles a combined_state_transition. Parameters ---------- state_combinations state_combinations to be combined. Returns ------- list Contains combinations of state_combinations of type tuple. """ return list(product(state_combinations, repeat=2)) def rate_assignment_standard( transition: pd.Series, transition_id: int, transition_rate_list: list[float], combined_state_transitions: Collection[tuple[int, ...]], ) -> list[float]: """ Adds a realizable combined_state_transition that is no energy transfer as a list to the transition_rate_list. Here, a combined_state_transition is realizable, if its first state_combination (i.e., current_state) and its second state_combination (i.e., future_state) have and only have a change in the state of one fluorophore that can also be found in the photophysical transition. Parameters ---------- transition The transition to be assigned to combined_state_transitions. transition_id The identity of Transition. transition_rate_list Destination of realizable combined_state_transitions. combined_state_transitions Contains combinations of state_combinations of type tuple. Returns ------- transition_rate_list : list The altered input parameter. """ source = transition["initial_state"].value destination = transition["final_state"].value for current_state, future_state in combined_state_transitions: for index in transition["fluorophore_ids"]: if source == current_state[index]: if destination == future_state[index]: future_state_part = future_state[:index] + future_state[index + 1 :] current_state_part = ( current_state[:index] + current_state[index + 1 :] ) if not future_state_part == current_state_part: break else: transition_rate_list.append( [ current_state, future_state, [index], transition["abbreviation"], transition_id, transition["rate"], transition["photon"], ] ) return transition_rate_list def rate_assignment_energy_transfer( transition: pd.Series, transition_id: int, transition_rate_list: list[float], combined_state_transitions: Collection[tuple[int, ...]], ) -> list[float]: """ Adds a realizable combined_state_transition that is also an energy transfer as a list to the transition_rate_list. Here, a combined_state_transition is realizable, if its first state_combination (i.e., current_state) and its second state_combination (i.e., future_state) have and only have a change in the state of two fluorophores that can also be found in the photophysical transition of the corresponding distance. Parameters ---------- transition The transition to be assigned to combined_state_transitions. transition_id The identity of Transition. transition_rate_list Destination of realizable combined_state_transitions. combined_state_transitions Contains combinations of state_combinations of type tuple. Returns ------- transition_rate_list : list The altered input parameter. """ source_donor, source_acceptor = transition["initial_state"].single_state_values destination_donor, destination_acceptor = transition[ "final_state" ].single_state_values for current_state, future_state in combined_state_transitions: for donor, acceptor in transition["fluorophore_ids"]: if ( source_donor == current_state[donor] and source_acceptor == current_state[acceptor] and destination_donor == future_state[donor] and destination_acceptor == future_state[acceptor] ): i, j = min(donor, acceptor), max(donor, acceptor) future_state_part = ( future_state[:i] + future_state[i + 1 : j] + future_state[j + 1 :] ) current_state_part = ( current_state[:i] + current_state[i + 1 : j] + current_state[j + 1 :] ) if not future_state_part == current_state_part: break else: transition_rate_list.append( [ current_state, future_state, [donor, acceptor], transition["abbreviation"], transition_id, transition["rate"], transition["photon"], ] ) return transition_rate_list def construct_transition_rate_list( transition_df: pd.DataFrame, combined_state_transitions: Collection[tuple[int, ...]] ) -> list[tuple[int, ...]]: """ Constructs a list that contains lists of each realizable combined_state_transition. The inner lists contain initial state_combination, final state_combination, the index of involved fluorophores, abbreviation, transition id, rate and whether a photon is emitted. Parameters ---------- transition_df Dataframe of all given transitions with non-zero rate containing their id as second level index and their other attributes as columns. Name of fluorophores as first level index. combined_state_transitions Contains combinations of state_combinations of type tuple. Returns ------- transition_rate_list : list Contains lists of each realizable combined_state_transition. """ transition_rate_list = list() for (_, identity), transition in transition_df.iterrows(): if isinstance(transition["initial_state"], SingleState): transition_rate_list = rate_assignment_standard( transition=transition, transition_id=identity, transition_rate_list=transition_rate_list, combined_state_transitions=combined_state_transitions, ) else: transition_rate_list = rate_assignment_energy_transfer( transition=transition, transition_id=identity, transition_rate_list=transition_rate_list, combined_state_transitions=combined_state_transitions, ) return transition_rate_list def construct_transition_matrix( combined_state_transitions_df: pd.DataFrame, ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """ Constructs a matrix of shape (combined_state_transitions_df.index.size, combined_state_transitions_df.index.size). The matrix is non-zero at a position, if the first index is a final_state in combined_state_transition_df which is the initial_state of the second index. In other words, the matrix is non-zero, if a transition (first index or row) can be followed by another transition (second index or column). Parameters ---------- combined_state_transitions_df Contains realizable combined_state_transitions with their id as index and their other attributes as columns. Returns ------- transition_matrix : np.ndarray Contains the normalized rate constants (i.e., point probabilities) for each possible combined_state_transition at the corresponding index pair. row_sums : np.ndarray Contains the sum of each row of non-normalized transition rates, i.e., the sum of rates of all possbile combined_state_transitions. """ transition_count = combined_state_transitions_df.index.size transition_rate_matrix = np.zeros( shape=(transition_count, transition_count), dtype=np.float64 ) for i, row in combined_state_transitions_df.iterrows(): final_state = row["final_state"] indices = combined_state_transitions_df[ combined_state_transitions_df["initial_state"] == final_state ].index transition_rate_matrix[i][indices] = combined_state_transitions_df["rate"][ indices ] row_sums = transition_rate_matrix.sum(axis=1, dtype=np.float64) row_sums_exp = np.tile(np.expand_dims(row_sums, axis=1), reps=row_sums.size) mask = np.ma.make_mask(row_sums_exp) transition_matrix = np.divide( transition_rate_matrix, row_sums_exp, out=np.zeros_like(transition_rate_matrix), where=mask, ) return transition_matrix, row_sums def derive_energy_transfer_transitions( donor_data: FluorophoreData, acceptor_data: FluorophoreData, fluorophore_ids: list[int], dipole_orientation_factor: float, distance: float, refractive_index: float, overwrite: dict[str, list[float]] | None = None, exclude: list[str] | None = None, include: dict[str, list[float]] | None = None, ) -> list[Transition]: """ Derive energy transfer transitions based on the experimental conditions and the fluorophore-combinations to be mimicked. The type of energy transfer is determined via the data file names. Parameters ---------- donor_data Contains all constant photophysical attributes of the donor. acceptor_data Contains all constant photophysical attributes of the acceptor. fluorophore_ids Contains the identities of all fluorophore pairs the transitions apply to as tuples. dipole_orientation_factor The dipole orientation factor of the fluorophore pair. distance The distance between the fluorophores of the fluorophore pair. refractive_index The refractive index of the medium. overwrite Contains the type of acceptor state as key and a list with a factor for the rate as well as an efficiency (of not recycling acceptor state) as value. exclude Contains the type of acceptor state (lowercase) to be excluded. include Contains the type of acceptor state as key and a list of tuples as values. The tuples contain the transition type and an efficiency. If the summed efficiencies is e.g., 0.5, all other energy transfers affecting the acceptor state are multiplied by 1-0.5. Returns ------- transitions : list[Transition] Contains energy transfer transitions of type Transition. """ data_dir = Path(__file__).parent / "fluorophore_spectra" donor_emission = pd.read_csv(data_dir / donor_data.data_files / "emission.csv") acceptor_files = sorted(Path(data_dir / acceptor_data.data_files).iterdir()) acceptor_abs_files = [ data_file for data_file in acceptor_files if data_file.name.startswith("absorption") ] emission_rate = fo.calculate_emission_rate( quantum_yield=donor_data.QUANTUM_YIELD, fluorescence_lifetime=donor_data.FLUORESCENCE_LIFETIME, ) minimum, maximum = 200, 1000 wavelengths_of_interest = np.arange(minimum, maximum + 1, 1, dtype=float) which_et = { "s0": [(TransitionType.FRET, 1)], "t1": [ ( TransitionType.S_T_ANNIHILATION, ( 1 - acceptor_data.STA_EFFICIENCY if overwrite is None or "t1" not in overwrite else 1 - overwrite["t1"][1] ), ), ( TransitionType.S_T_ANNI_RISC, ( acceptor_data.STA_EFFICIENCY if overwrite is None or "t1" not in overwrite else overwrite["t1"][1] ), ), ], "s1": [(TransitionType.S_S_ANNIHILATION, 1)], "cis": [ ( TransitionType.CIS_FRET_1, ( 1 - acceptor_data.BISO_EFFICIENCY if overwrite is None or "cis" not in overwrite else 1 - overwrite["cis"][1] ), ), ( TransitionType.CIS_FRET_2, ( acceptor_data.BISO_EFFICIENCY if overwrite is None or "cis" not in overwrite else overwrite["cis"][1] ), ), ], "off": [ ( TransitionType.OFF_FRET_1, ( 1 - acceptor_data.OFRET_EFFICIENCY if overwrite is None or "off" not in overwrite else 1 - overwrite["off"][1] ), ), ( TransitionType.OFF_FRET_2, ( acceptor_data.OFRET_EFFICIENCY if overwrite is None or "off" not in overwrite else overwrite["off"][1] ), ), ], } which_et_new = which_et.copy() if include is not None: for acceptor_state in include: which_et_new[acceptor_state] = [] total_factor = 0 for transition_type, factor in include[acceptor_state]: total_factor += factor which_et_new[acceptor_state].append((transition_type, factor)) for transition_type, factor in which_et[acceptor_state]: which_et_new[acceptor_state].append( (transition_type, factor * (1 - total_factor)) ) transitions = [] for acceptor_abs_file in acceptor_abs_files: acceptor_abs = pd.read_csv( Path(data_dir) / acceptor_data.data_files / acceptor_abs_file ) J = fo.calculate_spectral_overlap_integral( donor=donor_emission["y"], acceptor=acceptor_abs["y"], wavelengths=wavelengths_of_interest, ) rate = fo.calculate_fret_rate( distance=distance, emission_rate=emission_rate, spectral_overlap_integral=J, dipole_orientation_factor=dipole_orientation_factor, refractive_index=refractive_index, ) acceptor_state = acceptor_abs_file.name.split("_")[1].split(".")[0] if exclude is not None and acceptor_state in exclude: continue for transition_type, factor in which_et_new[acceptor_state]: if overwrite is not None and acceptor_state in overwrite: change_rate = overwrite[acceptor_state][0] else: change_rate = 1 transition = Transition( rate=rate * factor * change_rate, transition_type=transition_type, fluorophore_ids=fluorophore_ids, ) transitions.append(transition) return transitions def derive_transitions( summarize: bool = False, fluorophore_data: FluorophoreData | None = None, fluorophore_ids: list[int] | None = None, irradiance: float = 2, wavelength: float = 640, bleaching: bool = False, dstorm: bool = True, **dstorm_parameters, ) -> list[Transition]: """ Derive non-energy transfer transitions based on the experimental conditions and the fluorophore to be mimicked. Parameters ---------- summarize Whether to summarize some transitions into fewer. fluorophore_data Contains all constant photophysical attributes of the fluorophore. fluorophore_ids All identities of a fluorophore within a FluorophoreSystem. irradiance Irradiance in kW/cm². wavelength Wavelength in nm. bleaching Whether to incorporate bleaching as a possible transition. dstorm Whether to incorporate dstorm photoswitching as possible transitions. dstorm_parameters : fo.calculate_pet_rate arguments (except k_pet) Returns ------- transitions : list[Transition] Contains transitions of type Transition. """ fd = fluorophore_data _, _, frequency = fo.convert_wavenumber_wavelength_frequency(wavelength=wavelength) photon_flux = fo.calculate_photon_flux(irradiance=irradiance, frequency=frequency) path_absorption = ( Path(__file__).parent / "fluorophore_spectra" / fd.data_files / "absorption_s0.csv" ) if fluorophore_ids is None: fluorophore_ids = [0] dataframe_absorption = pd.read_csv(filepath_or_buffer=path_absorption, index_col=0) extinction_coefficient = dataframe_absorption.loc[int(wavelength), "y"] excitation_rate = fo.calculate_excitation_rate( photon_flux=photon_flux, extinction_coefficient=extinction_coefficient ) excitation = Transition( rate=excitation_rate, transition_type=TransitionType.EXCITATION, fluorophore_ids=fluorophore_ids, ) emission_rate = fo.calculate_emission_rate( quantum_yield=fd.QUANTUM_YIELD, fluorescence_lifetime=fd.FLUORESCENCE_LIFETIME ) emission = Transition( rate=emission_rate, transition_type=TransitionType.FLUORESCENT_EMISSION, fluorophore_ids=fluorophore_ids, ) isc_st = Transition( rate=fd.ISC_ST_RATE, transition_type=TransitionType.INTERSYSTEM_CROSSING_ST, fluorophore_ids=fluorophore_ids, ) isc_ts = Transition( rate=fd.ISC_TS_RATE, transition_type=TransitionType.INTERSYSTEM_CROSSING_TS, fluorophore_ids=fluorophore_ids, ) isomerization = Transition( rate=fd.ISO_RATE, transition_type=TransitionType.ISOMERIZATION, fluorophore_ids=fluorophore_ids, ) biso_rate = fo.calculate_excitation_rate( photon_flux=photon_flux, absorption_cross_section=fd.BISO_CROSS_SECTION ) photo_bisomerization = Transition( rate=biso_rate, transition_type=TransitionType.PHOTO_BISO, fluorophore_ids=fluorophore_ids, ) thermal_bisomerization = Transition( rate=fd.BISO_THERMAL_RATE, transition_type=TransitionType.THERM_BISO, fluorophore_ids=fluorophore_ids, ) internal_conversion_rate = fo.calculate_internal_conversion_rate( quantum_yield=fd.QUANTUM_YIELD, emission_rate=emission_rate, iso_rate=fd.ISO_RATE, isc_st_rate=fd.ISC_ST_RATE, ) internal_conversion = Transition( rate=internal_conversion_rate, transition_type=TransitionType.INTERNAL_CONVERSION_S, fluorophore_ids=fluorophore_ids, ) dstorm_transitions = [] if dstorm: dstorm_pet_t_rate = fo.calculate_pet_rate( k_pet=fd.DSTORM_PET_T_RATE_MOL, **dstorm_parameters ) dstorm_pet_s_rate = fo.calculate_pet_rate( k_pet=fd.DSTORM_PET_S_RATE_MOL, **dstorm_parameters ) dstorm_pet_t = Transition( rate=dstorm_pet_t_rate, transition_type=TransitionType.ET_CYCLE_T, fluorophore_ids=fluorophore_ids, ) dstorm_pet_s = Transition( rate=dstorm_pet_s_rate, transition_type=TransitionType.ET_CYCLE_S, fluorophore_ids=fluorophore_ids, ) dstorm_add_t_rate = dstorm_pet_t_rate * fd.DSTORM_PET_SUCCESS_RATE dstorm_add_s_rate = dstorm_pet_s_rate * fd.DSTORM_PET_SUCCESS_RATE dstorm_adduct_t = Transition( rate=dstorm_add_t_rate, transition_type=TransitionType.ADDUCT_T, fluorophore_ids=fluorophore_ids, ) dstorm_adduct_s = Transition( rate=dstorm_add_s_rate, transition_type=TransitionType.ADDUCT_S, fluorophore_ids=fluorophore_ids, ) photo_uncage = fo.calculate_excitation_rate( photon_flux=photon_flux, absorption_cross_section=fd.DSTORM_P_EL_CROSS_SECTION, ) photo_uncaging = Transition( rate=photo_uncage, transition_type=TransitionType.PHOTO_UNCAGING, fluorophore_ids=fluorophore_ids, ) thermal_elimination = Transition( rate=fd.DSTORM_TH_EL_RATE_1, transition_type=TransitionType.THERM_ELIMINATION, fluorophore_ids=fluorophore_ids, ) rad_escape = Transition( rate=dstorm_pet_t_rate * fd.RAD_ESCAPE_EFFICIENCY, transition_type=TransitionType.RAD_ESCAPE, fluorophore_ids=fluorophore_ids, ) rad_relax = Transition( rate=fd.RAD_RELAX_RATE, transition_type=TransitionType.RAD_RELAX, fluorophore_ids=fluorophore_ids, ) dstorm_transitions = [ dstorm_pet_t, dstorm_pet_s, dstorm_adduct_t, dstorm_adduct_s, photo_uncaging, thermal_elimination, rad_escape, rad_relax, ] bleach = [] if bleaching: bleach = [ Transition( rate=fd.PHOTOBLEACH_T1_RATE, transition_type=TransitionType.PHOTOBLEACHING_1, fluorophore_ids=fluorophore_ids, ) ] transitions = ( [ excitation, emission, isc_st, isc_ts, isomerization, photo_bisomerization, thermal_bisomerization, internal_conversion, ] + dstorm_transitions + bleach ) summarized_transitions = [ TransitionType.S1_S0_TRANSITIONS, TransitionType.T1_S0_TRANSITIONS, TransitionType.CIS_S0_TRANSITIONS, TransitionType.OFF_S0_TRANSITIONS, ] transitions_copy = transitions[:] if summarize: for summarized_transition in summarized_transitions: rate = 0 for transition in transitions_copy: if not transition.transition_type.photon: if ( transition.transition_type.initial_state == summarized_transition.initial_state and transition.transition_type.final_state == summarized_transition.final_state ): rate += transition.rate transitions.remove(transition) sum_transition = Transition( rate=rate, transition_type=summarized_transition, fluorophore_ids=fluorophore_ids, ) transitions.append(sum_transition) return transitions def interpolate_data( minimum_wavelength: int, maximum_wavelength: int, data: pd.DataFrame ) -> npt.NDArray[np.float64]: """ Interpolate missing data points from data. Parameters ---------- minimum_wavelength The minimum wavelength the interpolated data should cover. maximum_wavelength The maximum wavelength the interpolated data should cover. data Contains emission or absorption data with columns 'Wavelengths' and 'y'. Returns ------- interpolated : 1-D array_like The data corresponding to each integer wavelength within the defined margins. """ data["Wavelengths"] = data["Wavelengths"].astype(int) data = data.drop_duplicates(subset=["Wavelengths"]) wavelengths = data["Wavelengths"] add_lower = np.zeros(shape=min(wavelengths) - minimum_wavelength) add_upper = np.zeros(shape=maximum_wavelength - max(wavelengths)) wavelengths_stepwise = np.arange(min(wavelengths), max(wavelengths) + 1) interpolated = itp.CubicSpline(wavelengths, data["y"])(wavelengths_stepwise) interpolated = np.where(interpolated < 0, 0, interpolated) interpolated = np.concatenate((add_lower, interpolated, add_upper)) return interpolated