"""
Analysis of a photophysical simulation.
"""
from __future__ import annotations
import logging
import re
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any
import matplotlib as mpl
import numpy as np
import numpy.typing as npt
import pandas as pd
from . import figure as fi
from .miscellaneous import format_electronic_state, format_transition
if TYPE_CHECKING:
from matplotlib.axes import Axes as mplAxes
from fluopy.prediction import Prediction
from fluopy.simulation import Simulation
__all__: list[str] = ["Analysis"]
logger = logging.getLogger(__name__)
[docs]
class Analysis:
"""
Container of simulation-derived statistical attributes and methods.
Attributes
----------
simulation : fluopy.simulation.Simulation
Container for simulation-associated attributes.
frequency_transitions : 1-D array_like
Simulated relative frequencies of each transition.
frequency_states : dict
Name of fluorophores as keys and their state's simulated relative frequencies
(array) as values.
transition_time_distributions : Collection
Contains 1-D array_like for each transition (time until the transition).
lifetime_distributions : dict
Name of fluorophores as keys and collections of their state's simulated
lifetimes (1-D array_like) as values.
mean_transition_times : 1-D array_like
Simulated means of time until transition.
mean_lifetimes : dict
Name of fluorophores as keys and their state's simulated lifetime means (array)
as values.
state_occupations : dict
Name of fluorophores as keys and their state's simulated probability of being
occupied at any given point in time (array) as values.
"""
def __init__(self, simulation: Simulation) -> None:
"""
Parameters
----------
simulation
Container for simulation-associated attributes.
"""
if simulation.transition_series is None:
raise ValueError("analysis not available if simulation has not been run.")
self.simulation = simulation
absorbing = self.is_absorbing()
if absorbing:
logger.warning(
"if a fluorophore reaches its individual absorbing state, it has an "
"absolute state and transition frequency of 1, but the lifetime is nan "
"and the state occupation 0.",
stacklevel=2,
)
self.frequency_transitions = self.get_transition_occurrences()
self.frequency_states = self.get_state_occurrences()
self.transition_time_distributions, self.lifetime_distributions = (
self.get_lifetimes()
)
self.mean_transition_times = np.array(
[
(
np.mean(transition_time_distribution)
if transition_time_distribution.size > 0
else np.nan
)
for transition_time_distribution in self.transition_time_distributions
]
)
self.mean_lifetimes, self.state_occupations = self.infer_stats()
[docs]
def is_absorbing(self) -> bool:
"""
Check whether fluorophores reached Markovian absorbing states.
Returns
-------
is_abs : bool
Whether at least one of the fluorophores has reached a Markovian absorbing
state.
"""
from .transitions import SingleState
initial_states = self.simulation.transition_set.transition_df[
"initial_state"
].apply(lambda x: x.value if not isinstance(x.value, list) else None)
initial_states = initial_states.dropna().astype(int).values
absorbing_states = {}
is_abs = False
for (
fluorophore,
single_states,
) in self.simulation.transition_set.single_states.items():
for single_state in single_states:
if single_state not in initial_states:
if fluorophore in absorbing_states:
absorbing_states[fluorophore] += [single_state]
else:
absorbing_states[fluorophore] = [single_state]
for i, state_series in enumerate(self.simulation.state_series):
fluorophore = (
self.simulation.transition_set.fluorophore_system.fluorophores[i]
)
last_state = state_series[-1]
if fluorophore.name in absorbing_states:
if last_state in absorbing_states[fluorophore.name]:
is_abs = True
print(
f"fluorophore {i} has reached the Markovian absorbing state "
f"{SingleState(last_state)}"
)
return is_abs
[docs]
def get_transition_occurrences(self) -> npt.NDArray[np.float64]:
"""
Get the relative frequencies of transitions.
Returns
-------
frequency_transitions : npt.NDArray[np.float64]
Simulated relative frequencies of each transition.
"""
all_transition_occurrences = np.zeros(
shape=self.simulation.transition_set.transition_df.shape[0], dtype=np.int64
)
df = self.simulation.transition_set.combined_state_transitions_df
for _, i in self.simulation.transition_set.transition_df.index:
indices = df.index[df["transition_id"] == i].tolist()
transition_occurrences = np.isin(
self.simulation.transition_series, indices
).nonzero()[0]
all_transition_occurrences[i] = transition_occurrences.size
frequency_transitions = all_transition_occurrences.astype(np.float64)
grouper = {}
for (
fluorophore_comb,
group,
) in self.simulation.transition_set.transition_df.groupby(level=0, sort=False):
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, _, _ = match.group(1), match.group(2), match.group(3)
else:
d = fluorophore_comb
if d in grouper:
grouper[d] += group.index.get_level_values(1).tolist()
else:
grouper[d] = group.index.get_level_values(1).tolist()
for _, indices in grouper.items():
frequency_transitions[indices] /= np.sum(frequency_transitions[indices])
return frequency_transitions
[docs]
def get_state_occurrences(self) -> dict[str, npt.NDArray[np.float64]]:
"""
Get the relative frequencies of states.
Returns
-------
frequency_states : dict[str, npt.NDArray[np.float64]]
Name of fluorophores as keys and their state's simulated relative
frequencies (array) as values.
"""
single_states = self.simulation.transition_set.single_states
occurrences_states = {
key: np.zeros(len(value)) for key, value in single_states.items()
}
for i, state_series_fluorophore in enumerate(self.simulation.state_series):
fluorophore = (
self.simulation.transition_set.fluorophore_system.fluorophores[i].name
)
differences = np.diff(state_series_fluorophore)
changes_at = np.where(differences != 0)[0]
last_state = changes_at[-1] + 1
changes_at_and_last = np.append(changes_at, last_state)
states = state_series_fluorophore[changes_at_and_last]
state_ids, state_counts = np.unique(states, return_counts=True)
_, corresponding_indices, _ = np.intersect1d(
ar1=single_states[fluorophore],
ar2=state_ids,
assume_unique=True,
return_indices=True,
)
occurrences_states[fluorophore][corresponding_indices] += state_counts
frequency_states = {
key: array / np.sum(array) for key, array in occurrences_states.items()
}
return frequency_states
[docs]
def get_lifetimes(
self,
) -> tuple[list[npt.NDArray[np.float64]], dict[str, npt.NDArray[np.float64]]]:
"""
Get the lifetime distributions of states and the time until occurrence
distributions of transitions.
Note: if transition of interest is energy transfer, the time to transition is
only collected from the donor's point of view.
Returns
-------
transition_time_distributions : list[npt.NDArray[np.float64]]
Contains 1-D array_like for each transition (time until the transition).
lifetime_distributions : dict[str, npt.NDArray[np.float64]]
Name of fluorophores as keys and collections of their state's simulated
lifetimes (1-D array_like) as values.
"""
single_states = self.simulation.transition_set.single_states
df = self.simulation.transition_set.combined_state_transitions_df
lifetime_distributions = {
key: [np.array([]) for _ in range(len(value))]
for key, value in single_states.items()
}
transition_time_distributions = [
np.array([])
for _ in range(self.simulation.transition_set.transition_df.shape[0])
]
for i, state_series_fluorophore in enumerate(self.simulation.state_series):
fluorophore = (
self.simulation.transition_set.fluorophore_system.fluorophores[i].name
)
differences = np.diff(state_series_fluorophore)
changes_at = np.where(differences != 0)[0]
changed = changes_at + 1
initial_single_states = state_series_fluorophore[changes_at]
total_times = self.simulation.time_series[changed]
time_intervals = np.diff(total_times)
time_intervals = np.insert(arr=time_intervals, obj=0, values=total_times[0])
for j, state in enumerate(single_states[fluorophore]):
time_intervals_state = time_intervals[
np.where(initial_single_states == state)
]
lifetime_distributions[fluorophore][j] = np.concatenate(
[lifetime_distributions[fluorophore][j], time_intervals_state]
)
transitions_fluorophore = self.simulation.transition_series[changes_at]
for h, j in self.simulation.transition_set.transition_df.index:
indices = df.index[df["transition_id"] == j].tolist()
transition_occurrences = np.isin(
transitions_fluorophore, indices
).nonzero()[0]
if "dist" in h:
source_donor = self.simulation.transition_set.transition_df.loc[
(h, j), "initial_state"
].donor.value
donor_indices = np.where(initial_single_states == source_donor)[0]
transition_occurrences = transition_occurrences[
np.isin(transition_occurrences, donor_indices)
]
time_intervals_transition = time_intervals[transition_occurrences]
transition_time_distributions[j] = np.concatenate(
[transition_time_distributions[j], time_intervals_transition]
)
return transition_time_distributions, lifetime_distributions
[docs]
def infer_stats(
self,
) -> tuple[dict[str, npt.NDArray[np.float64]], dict[str, npt.NDArray[np.float64]]]:
"""
Infers statistics of states based on lifetime distributions and frequencies.
Returns
-------
mean_lifetimes : dict[str, npt.NDArray[np.float64]]
Name of fluorophores as keys and their state's simulated lifetime means
(array) as values.
state_occupations : dict[str, npt.NDArray[np.float64]]
Name of fluorophores as keys and their state's simulated probability of
being occupied at any given point in time (array) as values.
"""
mean_lifetimes = {}
state_occupations = {}
for fluorophore, distributions in self.lifetime_distributions.items():
mean_lifetimes[fluorophore] = np.array(
[
np.mean(distr) if distr.size != 0 else np.nan
for distr in distributions
]
)
state_occupations[fluorophore] = np.multiply(
self.frequency_states[fluorophore],
mean_lifetimes[fluorophore],
where=~np.isnan(mean_lifetimes[fluorophore]),
out=np.zeros(self.frequency_states[fluorophore].size),
)
state_occupations[fluorophore] /= state_occupations[fluorophore].sum()
return mean_lifetimes, state_occupations
[docs]
def get_fluorescence_lifetimes(
self, fluorophore: str | None = None
) -> npt.NDArray[np.float64]:
"""
Get the fluorescence lifetime (i.e., S1 lifetime) of the specified fluorophore.
Note that this does not consider whether the S1 state decays via photon
emission.
Parameters
----------
fluorophore
The name of the fluorophore whose fluorescence lifetime is to be returned.
Returns
-------
fluorescence_lifetimes : npt.NDArray[np.float64]
The fluorescence lifetimes of the specified fluorophore.
"""
s1_value = 1 # hardcoded but covered by tests
if fluorophore is not None:
if fluorophore not in self.lifetime_distributions:
raise ValueError(
f"fluorophore {fluorophore} not found in lifetime_distributions."
)
if len(self.lifetime_distributions) == 1:
fluorophore = list(self.lifetime_distributions.keys())[0]
else:
if fluorophore is None:
raise ValueError(
"if multiple fluorophores are present, fluorophore must be "
"specified."
)
s1_index = np.where(
self.simulation.transition_set.single_states[fluorophore] == s1_value
)[0][0]
fluorescence_lifetimes = self.lifetime_distributions[fluorophore][s1_index]
return fluorescence_lifetimes
[docs]
def get_emitting_transition_lifetimes(
self, fluorophore: str | None = None
) -> npt.NDArray[np.float64]:
"""
Get the lifetimes of the emitting transitions (i.e., S1 deexcitation via photon
emission) of the specified fluorophore.
Parameters
----------
fluorophore
The name of the fluorophore whose fluorescence lifetime is to be returned.
Returns
-------
exp_fluorescence_lifetimes : npt.NDArray[np.float64]
The fluorescence lifetimes (photon emssion) of the specified fluorophore.
"""
fluorophores = []
for key, _ in self.simulation.transition_set.single_states.items():
fluorophores.append(key)
if fluorophore is not None:
if fluorophore not in fluorophores:
raise ValueError(
f"fluorophore {fluorophore} not found in transition dataframe."
)
if len(fluorophores) == 1:
fluorophore = fluorophores[0]
else:
if fluorophore is None:
raise ValueError(
"if multiple fluorophores are present, fluorophore must be "
"specified."
)
sub_df = self.simulation.transition_set.transition_df.loc[fluorophore]
emitting_transitions_f = sub_df[sub_df["photon"]].index.to_numpy()
exp_fluorescence_lifetimes = [
self.transition_time_distributions[emitting_transition_f]
for emitting_transition_f in emitting_transitions_f
]
exp_fluorescence_lifetimes = np.concatenate(exp_fluorescence_lifetimes)
return exp_fluorescence_lifetimes
[docs]
def plot_frequency_transitions(
self,
prediction: Prediction | None = None,
diff_dist: bool = True,
**kwargs: Any,
) -> npt.NDArray[mplAxes]:
"""
Plot frequencies of transitions.
Parameters
----------
prediction
Container of mathematically derived statistical attributes and methods.
diff_dist
Whether to plot energy transfers distance-specific or not.
kwargs
kwargs for fluopy.figure.universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
df = self.simulation.transition_set.transition_df
dat = self.frequency_transitions
if not diff_dist:
df, dict2, dict_values = no_diff_dist(
transition_df=df,
fluorophores=self.simulation.transition_set.single_states.keys(),
)
data2 = np.delete(dat, dict_values)
for key, values in dict2.items():
data2[key] += np.sum(dat[values])
dat = data2
data = [np.arange(df.shape[0]), dat]
kwargs.setdefault("type_", "bar")
kwargs.setdefault("xlabel", None)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("edgecolor", "black")
kwargs.setdefault("xticks", range(df.shape[0]))
kwargs.setdefault(
"xticklabels",
dict(labels=df["abbreviation"].apply(format_transition), rotation=70),
)
colormap = mpl.colors.ListedColormap(
[
mpl.colormaps["Spectral"](value)
for value in np.linspace(0, 1, df.index.get_level_values(0).nunique())
]
)
kwargs.setdefault(
"color",
[
colormap(i)
for i, size in enumerate(df.groupby(level=0, sort=False).size())
for _ in range(size)
],
)
kwargs.setdefault("ylabel", "Prob. occurrence")
kwargs.setdefault("legend", True)
kwargs.setdefault(
"legendhandles",
[
mpl.patches.Patch(
color=colormap(i),
label=(
name.split("dist")[0][:-2]
if "dist" in name and not diff_dist
else name
),
)
for i, name in enumerate(df.index.get_level_values(0).unique())
],
)
draw_marker = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
draw_marker = [np.arange(df.shape[0]), prediction.frequency_transitions]
axes = fi.universal_figure(data=data, draw_marker=draw_marker, **kwargs)
return axes
[docs]
def plot_frequency_states(
self, prediction: Prediction | None = None, **kwargs: Any
) -> npt.NDArray[mplAxes]:
"""
Plot frequencies of states.
Parameters
----------
prediction
Container of mathematically derived statistical attributes and methods.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
from .transitions import SingleState
single_states = self.simulation.transition_set.single_states
colormap = mpl.colors.ListedColormap(
[
mpl.colormaps["Spectral"](value)
for value in np.linspace(0, 1, len(single_states))
]
)
colors, patches, xticks, data_merged, labels = [], [], 0, [], []
for i, (fluorophore, states) in enumerate(single_states.items()):
colors.extend([colormap(i) for _ in range(states.size)])
patches.append(mpl.patches.Patch(color=colormap(i), label=fluorophore))
xticks += states.size
data_merged.append(self.frequency_states[fluorophore])
labels.extend(
[
format_electronic_state(SingleState(identity).name)
for identity in states
]
)
data_merged = np.concatenate(data_merged)
data = [np.arange(xticks), data_merged]
kwargs.setdefault("type_", "bar")
kwargs.setdefault("xlabel", None)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("edgecolor", "black")
kwargs.setdefault("xticks", range(xticks))
kwargs.setdefault("xticklabels", dict(labels=labels, rotation=70))
kwargs.setdefault("ylabel", "Prob. occurrence")
kwargs.setdefault("color", colors)
kwargs.setdefault("legend", True)
kwargs.setdefault("legendhandles", patches)
draw_marker = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
draw_marker = [
np.arange(xticks),
np.concatenate(
[
prediction.frequency_states[fluorophore]
for fluorophore in single_states
]
),
]
axes = fi.universal_figure(data=data, draw_marker=draw_marker, **kwargs)
return axes
[docs]
def plot_mean_transition_times(
self,
prediction: Prediction | None = None,
diff_dist: bool = True,
**kwargs: Any,
) -> npt.NDArray[mplAxes]:
"""
Plot mean times until transitions occur.
Parameters
----------
prediction
Container of mathematically derived statistical attributes and methods.
diff_dist
Whether to plot energy transfers distance-specific or not.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
df = self.simulation.transition_set.transition_df
dat = self.mean_transition_times
if not diff_dist:
df, dict2, dict_values = no_diff_dist(
transition_df=df,
fluorophores=self.simulation.transition_set.single_states.keys(),
)
data2 = [
val
for i, val in enumerate(self.transition_time_distributions)
if i not in dict_values
]
for key, values in dict2.items():
for value in values:
data2[key] = np.concatenate(
(data2[key], self.transition_time_distributions[value])
)
dat = np.array([(np.mean(d) if d.size > 0 else np.nan) for d in data2])
data = [np.arange(df.shape[0]), dat]
kwargs.setdefault("type_", "bar")
kwargs.setdefault("xlabel", None)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("edgecolor", "black")
kwargs.setdefault("xticks", range(df.shape[0]))
kwargs.setdefault(
"xticklabels",
dict(labels=df["abbreviation"].apply(format_transition), rotation=70),
)
colormap = mpl.colors.ListedColormap(
[
mpl.colormaps["Spectral"](value)
for value in np.linspace(0, 1, df.index.get_level_values(0).nunique())
]
)
kwargs.setdefault(
"color",
[
colormap(i)
for i, size in enumerate(df.groupby(level=0, sort=False).size())
for _ in range(size)
],
)
kwargs.setdefault("ylabel", r"$\tau$ (s)")
kwargs.setdefault("legend", True)
kwargs.setdefault(
"legendhandles",
[
mpl.patches.Patch(
color=colormap(i),
label=(
name.split("dist")[0][:-2]
if "dist" in name and not diff_dist
else name
),
)
for i, name in enumerate(df.index.get_level_values(0).unique())
],
)
draw_marker = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
if prediction.energy_transfer:
raise ValueError(
"predicted mean_transition_times not available if energy transfer "
"possible."
)
draw_marker = [np.arange(df.shape[0]), prediction.mean_transition_times]
axes = fi.universal_figure(data=data, draw_marker=draw_marker, **kwargs)
return axes
[docs]
def plot_mean_lifetimes(
self, prediction: Prediction | None = None, **kwargs: Any
) -> npt.NDArray[mplAxes]:
"""
Plot mean lifetimes of states.
Parameters
----------
prediction
Container of mathematically derived statistical attributes and methods.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
from .transitions import SingleState
single_states = self.simulation.transition_set.single_states
colormap = mpl.colors.ListedColormap(
[
mpl.colormaps["Spectral"](value)
for value in np.linspace(0, 1, len(single_states))
]
)
colors, patches, xticks, data_merged, labels = [], [], 0, [], []
for i, (fluorophore, states) in enumerate(single_states.items()):
colors.extend([colormap(i) for _ in range(states.size)])
patches.append(mpl.patches.Patch(color=colormap(i), label=fluorophore))
xticks += states.size
data_merged.append(self.mean_lifetimes[fluorophore])
labels.extend(
[
format_electronic_state(SingleState(identity).name)
for identity in states
]
)
data_merged = np.concatenate(data_merged)
data = [np.arange(xticks), data_merged]
kwargs.setdefault("type_", "bar")
kwargs.setdefault("xlabel", None)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("edgecolor", "black")
kwargs.setdefault("xticks", range(xticks))
kwargs.setdefault("xlim", [-1, xticks])
kwargs.setdefault("xticklabels", dict(labels=labels, rotation=70))
kwargs.setdefault("color", colors)
kwargs.setdefault("legend", True)
kwargs.setdefault("legendhandles", patches)
kwargs.setdefault("ylabel", r"$\tau$ (s)")
draw_marker = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
if prediction.energy_transfer:
raise ValueError(
"predicted lifetime_distributions not available if energy "
"transfers possible."
)
draw_marker = [
np.arange(xticks),
np.concatenate(
[
prediction.mean_lifetimes[fluorophore]
for fluorophore in single_states
]
),
]
axes = fi.universal_figure(data=data, draw_marker=draw_marker, **kwargs)
return axes
[docs]
def plot_state_occupations(
self, prediction: Prediction | None = None, **kwargs: Any
) -> npt.NDArray[mplAxes]:
"""
Plot state occupation times (relative total time spent in state).
Parameters
----------
prediction
Container of mathematically derived statistical attributes and methods.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
from .transitions import SingleState
single_states = self.simulation.transition_set.single_states
colormap = mpl.colors.ListedColormap(
[
mpl.colormaps["Spectral"](value)
for value in np.linspace(0, 1, len(single_states))
]
)
colors, patches, xticks, data_merged, labels = [], [], 0, [], []
for i, (fluorophore, states) in enumerate(single_states.items()):
colors.extend([colormap(i) for _ in range(states.size)])
patches.append(mpl.patches.Patch(color=colormap(i), label=fluorophore))
xticks += states.size
data_merged.append(self.state_occupations[fluorophore])
labels.extend(
[
format_electronic_state(SingleState(identity).name)
for identity in states
]
)
data_merged = np.concatenate(data_merged)
data = [np.arange(xticks), data_merged]
kwargs.setdefault("type_", "bar")
kwargs.setdefault("xlabel", None)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("edgecolor", "black")
kwargs.setdefault("xticks", range(xticks))
kwargs.setdefault("xticklabels", dict(labels=labels, rotation=70))
kwargs.setdefault("ylabel", "Prob. occupation")
kwargs.setdefault("color", colors)
kwargs.setdefault("legend", True)
kwargs.setdefault("legendhandles", patches)
draw_marker = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
if prediction.energy_transfer:
raise ValueError(
"predicted state_occupations not available if energy transfers "
"possible."
)
draw_marker = [
np.arange(xticks),
np.concatenate(
[
prediction.state_occupations[fluorophore]
for fluorophore in single_states
]
),
]
axes = fi.universal_figure(data=data, draw_marker=draw_marker, **kwargs)
return axes
[docs]
def plot_lifetime_distributions(
self,
fluorophore: str,
state_identity: int,
prediction: Prediction | None = None,
**kwargs: Any,
) -> npt.NDArray[mplAxes]:
"""
Plot lifetime distributions of states.
Parameters
----------
fluorophore
The name of the fluorophore whose state's distribution is to be shown.
state_identity
The identity of the state whose distribution is to be shown.
prediction
Container of mathematically derived statistical attributes and methods.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
from .transitions import SingleState
kwargs.setdefault("type_", "hist")
kwargs.setdefault("ylabel", "Prob. density")
kwargs.setdefault("title", f"{fluorophore}")
kwargs.setdefault("yscale", "log")
kwargs.setdefault(
"xlabel",
rf"{format_electronic_state(SingleState(state_identity).name)}"
" duration (s)",
)
kwargs.setdefault("density", True)
index = np.where(
self.simulation.transition_set.single_states[fluorophore] == state_identity
)[0][0]
data = self.lifetime_distributions[fluorophore][index]
plot_distribution = None
plot_distribution_label = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
if prediction.energy_transfer:
raise ValueError(
"predicted lifetime_distributions not available if energy transfer "
"possible."
)
plot_distribution = prediction.lifetime_distributions[fluorophore][index]
plot_distribution_label = "Prediction"
kwargs.setdefault("legend", True)
axes = fi.universal_figure(
data=data,
plot_distribution=plot_distribution,
plot_distribution_label=plot_distribution_label,
**kwargs,
)
return axes
[docs]
def plot_transition_time_distributions(
self,
fluorophore: str,
transition_id: int,
prediction: Prediction | None = None,
**kwargs: Any,
) -> npt.NDArray[mplAxes]:
"""
Plot distributions of time until transition occurs.
Parameters
----------
fluorophore
The name of the fluorophore whose transition's distribution is to be shown.
transition_id
The identity of the transition whose distribution is to be shown.
prediction
Container of mathematically derived statistical attributes and methods.
kwargs
kwargs to universal_figure
Returns
-------
npt.NDArray[mplAxes]
Contains matplotlib.axes._subplots.AxesSubplots.
"""
kwargs.setdefault("type_", "hist")
kwargs.setdefault("ylabel", "PD")
kwargs.setdefault(
"title",
rf"""$\tau$ of {fluorophore}
{format_transition(self.simulation.transition_set.transition_df.loc[(fluorophore,
transition_id),
"abbreviation"])}""",
)
kwargs.setdefault("yscale", "log")
kwargs.setdefault("xlabel", "time to transition [s]")
kwargs.setdefault("density", True)
data = self.transition_time_distributions[transition_id]
plot_distribution = None
plot_distribution_label = None
if prediction is not None:
if prediction.transition_set is not self.simulation.transition_set:
logger.warning(
"prediction is based on different TransitionSet than simulation.",
stacklevel=2,
)
if prediction.energy_transfer:
raise ValueError(
"predicted transition_time_distributions not available if energy "
"transfer possible."
)
plot_distribution = prediction.transition_time_distributions[transition_id]
plot_distribution_label = "pred"
kwargs.setdefault("label", "sim")
kwargs.setdefault("legend", True)
axes = fi.universal_figure(
data=data,
plot_distribution=plot_distribution,
plot_distribution_label=plot_distribution_label,
**kwargs,
)
return axes
def no_diff_dist(
transition_df: pd.DataFrame, fluorophores: Iterable[str]
) -> tuple[pd.DataFrame, dict[str, Any], npt.NDArray[np.float64]]:
"""
Get a transition_df which only contains one distance for each type of energy
transfer.
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.
fluorophores
Names of fluorophores.
Returns
-------
df2 : pd.DataFrame
Altered transition_df.
dict2 : dict[str, Any]
New indices of type of energy transfer as keys and the old indices of distance-
dependent duplices as values.
dict2_vals : npt.NDArray[np.float64]
Flattened array of the values of dict2.
"""
df2 = transition_df.copy()
level_0 = df2.index.get_level_values(0)
level_1 = df2.index.get_level_values(1)
level_0_unique = level_0.unique()
dict1 = {}
for f in fluorophores:
indices = np.where(level_0_unique.str.contains(f))[0]
different_names = level_0_unique[indices]
indices_2 = np.where(different_names.str.contains("dist"))[0]
to_discard = different_names[indices_2[1:]]
to_keep = different_names[indices_2[0]]
corresponding_level1 = level_1[level_0.isin(to_discard)]
dict1[to_keep] = corresponding_level1
df2 = df2[~df2.index.get_level_values(0).isin(to_discard)]
df2.index = pd.MultiIndex.from_arrays(
[df2.index.get_level_values(0), range(len(df2))]
)
dict2 = {}
for key, values in dict1.items():
df_subset = df2.loc[key]
size = df_subset.shape[0]
for i in range(size):
dict2[df_subset.index[i]] = values[i::size]
dict2_vals = np.concatenate([v.to_numpy() for v in dict2.values()])
return df2, dict2, dict2_vals