Line data Source code
1 : # SPDX-FileCopyrightText: 2025 Pairinteraction Developers
2 : # SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 0 : import logging
5 0 : from typing import TYPE_CHECKING
6 :
7 0 : import mplcursors
8 0 : import numpy as np
9 :
10 0 : from pairinteraction_gui.config import KetConfigLifetimes
11 0 : from pairinteraction_gui.page.base_page import SimulationPage
12 0 : from pairinteraction_gui.plotwidget.plotwidget import PlotWidget
13 0 : from pairinteraction_gui.qobjects import show_status_tip
14 :
15 : if TYPE_CHECKING:
16 : import pairinteraction.real as pi
17 :
18 0 : logger = logging.getLogger(__name__)
19 :
20 :
21 0 : class LifetimesPage(SimulationPage):
22 : """Page for calculating lifetimes."""
23 :
24 0 : title = "Lifetimes"
25 0 : tooltip = "Calculate the lifetimes and transition rates for a specified ket."
26 :
27 0 : def setupWidget(self) -> None:
28 0 : self.plotwidget = PlotWidget(self)
29 0 : self.layout().addWidget(self.plotwidget)
30 0 : super().setupWidget()
31 :
32 0 : show_status_tip(self, "Ready", timeout=1)
33 :
34 : # all attributes of instance BaseConfig will be added to the toolbox in postSetupWidget
35 0 : self.ket_config = KetConfigLifetimes(self)
36 :
37 0 : def calculate(self) -> None:
38 : # since this calculation is rather fast, we can just call it directly and dont have to use a different process
39 0 : ket = self.ket_config.get_ket_atom(0)
40 0 : temperature = self.ket_config.get_temperature()
41 0 : self.kets_sp, self.transition_rates_sp = ket.get_spontaneous_transition_rates(unit="1/ms")
42 0 : self.kets_bbr, self.transition_rates_bbr = ket.get_black_body_transition_rates(temperature, "K", unit="1/ms")
43 :
44 0 : def update_plot(self) -> None:
45 0 : ax = self.plotwidget.canvas.ax
46 0 : ax.clear()
47 :
48 0 : n_list = np.arange(0, np.max([s.n for s in self.kets_bbr]) + 1)
49 0 : sorted_rates: dict[str, dict[int, list[tuple[pi.KetAtom, float]]]] = {}
50 0 : for key, kets, rates in [
51 : ("BBR", self.kets_bbr, self.transition_rates_bbr),
52 : ("SP", self.kets_sp, self.transition_rates_sp),
53 : ]:
54 0 : sorted_rates[key] = {n: [] for n in n_list}
55 0 : for i, s in enumerate(kets):
56 0 : sorted_rates[key][s.n].append((s, rates[i]))
57 0 : self.sorted_rates = sorted_rates
58 :
59 0 : rates_summed = {key: [sum(rates for _, rates in sorted_rates[key][n]) for n in n_list] for key in sorted_rates}
60 0 : bar_sp = ax.bar(n_list, rates_summed["SP"], label="Spontaneous Decay", color="blue", alpha=0.8)
61 0 : bar_bbr = ax.bar(n_list, rates_summed["BBR"], label="Black Body Radiation", color="red", alpha=0.8)
62 0 : self.artists = (bar_sp, bar_bbr)
63 0 : ax.legend()
64 :
65 0 : ax.set_xlabel("Principal Quantum Number $n$")
66 0 : ax.set_ylabel(r"Transition Rates (1 / ms)")
67 :
68 0 : self.add_cursor()
69 :
70 0 : self.plotwidget.canvas.draw()
71 :
72 0 : def add_cursor(self) -> None:
73 : """Add interactive cursor to the plot."""
74 : # Remove any existing cursors to avoid duplicates
75 0 : if hasattr(self, "mpl_cursor"):
76 0 : if hasattr(self.mpl_cursor, "remove"): # type: ignore
77 0 : self.mpl_cursor.remove() # type: ignore
78 0 : del self.mpl_cursor # type: ignore
79 :
80 0 : self.mpl_cursor = mplcursors.cursor(
81 : self.artists,
82 : hover=mplcursors.HoverMode.Transient,
83 : annotation_kwargs={
84 : "bbox": {"boxstyle": "round,pad=0.5", "fc": "white", "alpha": 0.9, "ec": "gray"},
85 : "arrowprops": {"arrowstyle": "->", "connectionstyle": "arc3", "color": "gray"},
86 : },
87 : )
88 :
89 0 : @self.mpl_cursor.connect("add")
90 0 : def on_add(sel: mplcursors.Selection) -> None:
91 0 : label = sel.artist.get_label()
92 0 : x, y, width, height = sel.artist[sel.index].get_bbox().bounds
93 :
94 0 : n = round(x + width / 2)
95 0 : key = "BBR" if "Black Body" in label else "SP"
96 0 : state_text = "\n".join(f"{s}: {r:.5f}/ms" for (s, r) in self.sorted_rates[key][n])
97 0 : text = f"{label} to n={n}:\n{state_text}"
98 :
99 0 : sel.annotation.set(text=text, position=(0, 20), anncoords="offset points")
100 0 : sel.annotation.xy = (x + width / 2, y + height / 2)
|