jablonski 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jablonski/__init__.py +28 -0
- jablonski/_typing.py +35 -0
- jablonski/_units.py +20 -0
- jablonski/plots/jablonski_diagrams.py +371 -0
- jablonski/plots/plots.py +131 -0
- jablonski/simulation.py +318 -0
- jablonski/states.py +111 -0
- jablonski/sweeps.py +53 -0
- jablonski/tests/test_simulation.py +177 -0
- jablonski/tests/test_sweeps.py +42 -0
- jablonski/tests/test_transitions.py +334 -0
- jablonski/transitions.py +317 -0
- jablonski/util.py +96 -0
- jablonski-0.1.0.dist-info/METADATA +21 -0
- jablonski-0.1.0.dist-info/RECORD +16 -0
- jablonski-0.1.0.dist-info/WHEEL +4 -0
jablonski/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jablonski
|
|
3
|
+
~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Write and simulate ODE systems to describe the transitions in spectroscopic
|
|
6
|
+
molecular systems.
|
|
7
|
+
|
|
8
|
+
:copyright: 2024 by jablonski Authors, see AUTHORS for more details.
|
|
9
|
+
:license: BSD, see LICENSE for more details.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from poincare import Parameter, Simulator, assign
|
|
13
|
+
|
|
14
|
+
from . import simulation, transitions, util
|
|
15
|
+
from .states import SingletState, SpectroscopicSystem, TripletState, initial
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"transitions",
|
|
19
|
+
"simulation",
|
|
20
|
+
"util",
|
|
21
|
+
"SpectroscopicSystem",
|
|
22
|
+
"initial",
|
|
23
|
+
"TripletState",
|
|
24
|
+
"SingletState",
|
|
25
|
+
"assign",
|
|
26
|
+
"Parameter",
|
|
27
|
+
"Simulator",
|
|
28
|
+
]
|
jablonski/_typing.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jablonski._typing
|
|
3
|
+
~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Types and type alias.
|
|
6
|
+
|
|
7
|
+
:copyright: 2024 by jablonski Authors, see AUTHORS for more details.
|
|
8
|
+
:license: BSD, see LICENSE for more details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Literal, Protocol, TypeAlias, runtime_checkable
|
|
12
|
+
|
|
13
|
+
import pint
|
|
14
|
+
from poincare import Parameter, Variable
|
|
15
|
+
|
|
16
|
+
Power: TypeAlias = float | int
|
|
17
|
+
Time: TypeAlias = float | int | pint.Quantity
|
|
18
|
+
|
|
19
|
+
SpinMultiplicity = Literal["singlet", "triplet"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class Pumper(Protocol):
|
|
24
|
+
pump: Parameter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class RadiativeDecay(Protocol):
|
|
29
|
+
radiative_decay: Parameter
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class Drawable(Protocol):
|
|
34
|
+
_source: Variable
|
|
35
|
+
_target: Variable
|
jablonski/_units.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jablonski._units
|
|
3
|
+
~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Units and dimensions.
|
|
6
|
+
|
|
7
|
+
:copyright: 2024 by jablonski Authors, see AUTHORS for more details.
|
|
8
|
+
:license: BSD, see LICENSE for more details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pint
|
|
12
|
+
|
|
13
|
+
ureg = pint.get_application_registry()
|
|
14
|
+
|
|
15
|
+
DEFAULT_DELTA = 50 * ureg.picosecond
|
|
16
|
+
|
|
17
|
+
DIM_WAVELENGTH = {"[length]": 1}
|
|
18
|
+
DIM_WAVENUMBER = {"[length]": -1}
|
|
19
|
+
DIM_FREQUENCY = {"[time]": -1}
|
|
20
|
+
DIM_ENERGY = ureg.get_dimensionality("eV")
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from itertools import chain
|
|
2
|
+
from typing import Iterable, Literal, Mapping, NamedTuple
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib.axes import Axes
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
|
|
9
|
+
Number = float | int
|
|
10
|
+
column = str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Level(NamedTuple):
|
|
14
|
+
label: str
|
|
15
|
+
energy: Number
|
|
16
|
+
column: str
|
|
17
|
+
color: str = "black"
|
|
18
|
+
linewidth: Number = 2.5
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Transition(NamedTuple):
|
|
22
|
+
source: Level
|
|
23
|
+
target: Level
|
|
24
|
+
radiative: bool
|
|
25
|
+
label: str | None = None
|
|
26
|
+
color: str = "royalblue"
|
|
27
|
+
linewidth: Number = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JablonskiDiagram:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
levels: Iterable[Level],
|
|
34
|
+
transitions: Iterable[Transition],
|
|
35
|
+
columns: Iterable[column] = ["singlet", "triplet"],
|
|
36
|
+
):
|
|
37
|
+
self.levels = levels
|
|
38
|
+
self.transitions = transitions
|
|
39
|
+
self.columns = columns
|
|
40
|
+
|
|
41
|
+
def _place_columns(
|
|
42
|
+
self, columns: Iterable[column]
|
|
43
|
+
) -> Mapping[column, tuple[Number, Number]]:
|
|
44
|
+
num_columns = len(columns)
|
|
45
|
+
# Spaces between levels are 1/4 level width, calculated automatically to use all of xscale
|
|
46
|
+
# level_lenght l is the solution to n*l + (n-1)*l/4 = xscale, n = num_column
|
|
47
|
+
level_length = self._xscale / (5 / 4 * num_columns - 1 / 4)
|
|
48
|
+
interval_length = level_length / 4
|
|
49
|
+
column_positions = {
|
|
50
|
+
col: (
|
|
51
|
+
i * interval_length + i * level_length,
|
|
52
|
+
i * interval_length + (i + 1) * level_length,
|
|
53
|
+
)
|
|
54
|
+
for i, col in enumerate(columns)
|
|
55
|
+
}
|
|
56
|
+
return column_positions
|
|
57
|
+
|
|
58
|
+
def _build_positions(
|
|
59
|
+
self, column_positions: Mapping[column, tuple[Number, Number]]
|
|
60
|
+
) -> Mapping[column, tuple[tuple[Number, Number], Number]]:
|
|
61
|
+
|
|
62
|
+
positions = {}
|
|
63
|
+
for level in self.levels:
|
|
64
|
+
positions[level] = (
|
|
65
|
+
column_positions[level.column],
|
|
66
|
+
level.energy,
|
|
67
|
+
)
|
|
68
|
+
return positions
|
|
69
|
+
|
|
70
|
+
def _sort_transitions(
|
|
71
|
+
self, column_positions: Mapping[column, tuple[Number, Number]]
|
|
72
|
+
) -> tuple[
|
|
73
|
+
Mapping[column, Mapping[Literal["up", "down"], Iterable[Transition]]],
|
|
74
|
+
Mapping[column, Mapping[Literal["forward", "backward"], Iterable[Transition]]],
|
|
75
|
+
]:
|
|
76
|
+
intracolumn_transitions = {
|
|
77
|
+
column: [
|
|
78
|
+
t
|
|
79
|
+
for t in self.transitions
|
|
80
|
+
if t.source.column == column and t.target.column == column
|
|
81
|
+
]
|
|
82
|
+
for column in self.columns
|
|
83
|
+
}
|
|
84
|
+
multicolumn_transitions = {
|
|
85
|
+
column: [
|
|
86
|
+
t
|
|
87
|
+
for t in self.transitions
|
|
88
|
+
if t.source.column == column and t.source.column != t.target.column
|
|
89
|
+
]
|
|
90
|
+
for column in self.columns
|
|
91
|
+
}
|
|
92
|
+
sorted_intracolumn_transitions = {
|
|
93
|
+
column: {
|
|
94
|
+
"up": sorted(
|
|
95
|
+
[
|
|
96
|
+
t
|
|
97
|
+
for t in intracolumn_transitions[column]
|
|
98
|
+
if t.target.energy >= t.source.energy
|
|
99
|
+
],
|
|
100
|
+
key=lambda t: (t.source.energy, -t.target.energy),
|
|
101
|
+
),
|
|
102
|
+
"down": sorted(
|
|
103
|
+
[
|
|
104
|
+
t
|
|
105
|
+
for t in intracolumn_transitions[column]
|
|
106
|
+
if t.target.energy < t.source.energy
|
|
107
|
+
],
|
|
108
|
+
key=lambda t: (t.target.energy, -t.source.energy),
|
|
109
|
+
),
|
|
110
|
+
}
|
|
111
|
+
for column in self.columns
|
|
112
|
+
}
|
|
113
|
+
sorted_multicolumn_transitions = {
|
|
114
|
+
column: {
|
|
115
|
+
"backward": [
|
|
116
|
+
t
|
|
117
|
+
for t in multicolumn_transitions[column]
|
|
118
|
+
if column_positions[t.source.column][0]
|
|
119
|
+
>= column_positions[t.target.column][0]
|
|
120
|
+
],
|
|
121
|
+
"forward": [
|
|
122
|
+
t
|
|
123
|
+
for t in multicolumn_transitions[column]
|
|
124
|
+
if column_positions[t.source.column][0]
|
|
125
|
+
< column_positions[t.target.column][0]
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
for column in self.columns
|
|
129
|
+
}
|
|
130
|
+
return sorted_intracolumn_transitions, sorted_multicolumn_transitions
|
|
131
|
+
|
|
132
|
+
def plot(
|
|
133
|
+
self,
|
|
134
|
+
figsize: tuple[Number, Number] = (
|
|
135
|
+
6.4,
|
|
136
|
+
4.8,
|
|
137
|
+
),
|
|
138
|
+
fontsize: Number = 10,
|
|
139
|
+
show_energy_axis: bool = True,
|
|
140
|
+
) -> tuple[Axes, Figure]:
|
|
141
|
+
|
|
142
|
+
# Use energy ranges and figure size
|
|
143
|
+
energies = [level.energy for level in self.levels]
|
|
144
|
+
max_energy = np.max(energies)
|
|
145
|
+
min_energy = np.min(energies)
|
|
146
|
+
self._yscale = max_energy - min_energy
|
|
147
|
+
self._xscale = self._yscale * figsize[0] / figsize[1]
|
|
148
|
+
|
|
149
|
+
column_positions = self._place_columns(self.columns)
|
|
150
|
+
positions = self._build_positions(column_positions)
|
|
151
|
+
intracolumn, multicolumn = self._sort_transitions(column_positions)
|
|
152
|
+
|
|
153
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
154
|
+
|
|
155
|
+
# Draw energy levels
|
|
156
|
+
for level, ((x_start, x_end), y) in positions.items():
|
|
157
|
+
ax.hlines(
|
|
158
|
+
y,
|
|
159
|
+
x_start,
|
|
160
|
+
x_end,
|
|
161
|
+
linewidth=level.linewidth,
|
|
162
|
+
color=level.color,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
ax.text(
|
|
166
|
+
x_end + 0.015 * self._xscale,
|
|
167
|
+
y,
|
|
168
|
+
level.label,
|
|
169
|
+
va="center",
|
|
170
|
+
fontsize=fontsize,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Draw transitions
|
|
174
|
+
for column in self.columns:
|
|
175
|
+
x_start, x_end = column_positions[column]
|
|
176
|
+
n_intra = len(intracolumn[column]["up"]) + len(intracolumn[column]["down"])
|
|
177
|
+
spacing = (x_end - x_start) / (n_intra + 1)
|
|
178
|
+
|
|
179
|
+
# draw intracolumn transitions
|
|
180
|
+
for i, transition in enumerate(
|
|
181
|
+
chain(intracolumn[column]["up"], intracolumn[column]["down"])
|
|
182
|
+
):
|
|
183
|
+
self._draw_transition(
|
|
184
|
+
ax,
|
|
185
|
+
(
|
|
186
|
+
x_start + (i + 1) * spacing,
|
|
187
|
+
transition.source.energy,
|
|
188
|
+
),
|
|
189
|
+
(
|
|
190
|
+
x_start + (i + 1) * spacing,
|
|
191
|
+
transition.target.energy,
|
|
192
|
+
),
|
|
193
|
+
label=transition.label,
|
|
194
|
+
radiative=transition.radiative,
|
|
195
|
+
color=transition.color,
|
|
196
|
+
linewidth=transition.linewidth,
|
|
197
|
+
)
|
|
198
|
+
for transition in multicolumn[column]["backward"]:
|
|
199
|
+
self._draw_transition(
|
|
200
|
+
ax,
|
|
201
|
+
(x_start, transition.source.energy),
|
|
202
|
+
(
|
|
203
|
+
column_positions[transition.target.column][1],
|
|
204
|
+
transition.target.energy,
|
|
205
|
+
),
|
|
206
|
+
label=transition.label,
|
|
207
|
+
radiative=transition.radiative,
|
|
208
|
+
color=transition.color,
|
|
209
|
+
linewidth=transition.linewidth,
|
|
210
|
+
)
|
|
211
|
+
for transition in multicolumn[column]["forward"]:
|
|
212
|
+
self._draw_transition(
|
|
213
|
+
ax,
|
|
214
|
+
(x_end, transition.source.energy),
|
|
215
|
+
(
|
|
216
|
+
column_positions[transition.target.column][0],
|
|
217
|
+
transition.target.energy,
|
|
218
|
+
),
|
|
219
|
+
label=transition.label,
|
|
220
|
+
radiative=transition.radiative,
|
|
221
|
+
color=transition.color,
|
|
222
|
+
linewidth=transition.linewidth,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Formatting
|
|
226
|
+
ax.set_xlim(-0.05 * self._xscale, self._xscale * 1.1)
|
|
227
|
+
ax.set_xticks([])
|
|
228
|
+
|
|
229
|
+
ax.set_ylim(
|
|
230
|
+
min_energy - self._yscale * 0.05,
|
|
231
|
+
max_energy + self._yscale * 0.05,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
ax.xaxis.set_visible(False)
|
|
235
|
+
|
|
236
|
+
if show_energy_axis:
|
|
237
|
+
ax.set_ylabel("Energy")
|
|
238
|
+
ax.set_yticks(energies)
|
|
239
|
+
else:
|
|
240
|
+
ax.set_yticks([])
|
|
241
|
+
|
|
242
|
+
fig.tight_layout()
|
|
243
|
+
return fig, ax
|
|
244
|
+
|
|
245
|
+
def _draw_transition(
|
|
246
|
+
self,
|
|
247
|
+
ax: Axes,
|
|
248
|
+
start: tuple[Number, Number],
|
|
249
|
+
end: tuple[Number, Number],
|
|
250
|
+
label: str | None,
|
|
251
|
+
radiative: bool,
|
|
252
|
+
color="royalblue",
|
|
253
|
+
linewidth: Number = 1,
|
|
254
|
+
):
|
|
255
|
+
x1, y1 = start
|
|
256
|
+
x2, y2 = end
|
|
257
|
+
|
|
258
|
+
if radiative:
|
|
259
|
+
ax.annotate(
|
|
260
|
+
"",
|
|
261
|
+
xy=(x2, y2),
|
|
262
|
+
xytext=(x1, y1),
|
|
263
|
+
arrowprops=dict(
|
|
264
|
+
arrowstyle="->",
|
|
265
|
+
linestyle="-",
|
|
266
|
+
color=color,
|
|
267
|
+
lw=linewidth,
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
else:
|
|
272
|
+
# "Squiggly" lines are a straight line with a pieceswise modulation: 1. sine wave, 2: interpolating spline, 3: straight line.
|
|
273
|
+
dx = x2 - x1
|
|
274
|
+
dy = y2 - y1
|
|
275
|
+
length = np.hypot(dx, dy)
|
|
276
|
+
|
|
277
|
+
if length == 0:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
px = -dy / length
|
|
281
|
+
py = dx / length
|
|
282
|
+
amplitude = 0.01 * (self._xscale * np.abs(px) + self._yscale * np.abs(py))
|
|
283
|
+
frequency = 12
|
|
284
|
+
|
|
285
|
+
# Define the time splits (t goes from 0 to 1)
|
|
286
|
+
t1 = 0.85 # Spline start
|
|
287
|
+
t2 = 0.90 # Straight line start
|
|
288
|
+
|
|
289
|
+
t_sine = np.linspace(0, t1, 200)
|
|
290
|
+
wiggle_sine = amplitude * np.sin(2 * np.pi * frequency * t_sine)
|
|
291
|
+
|
|
292
|
+
# Boundary conditions for spline
|
|
293
|
+
w1 = amplitude * np.sin(2 * np.pi * frequency * t1)
|
|
294
|
+
dw1_dt = (
|
|
295
|
+
amplitude * (2 * np.pi * frequency) * np.cos(2 * np.pi * frequency * t1)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Calculate spline
|
|
299
|
+
t_blend = np.linspace(t1, t2, 50)
|
|
300
|
+
u = (t_blend - t1) / (t2 - t1)
|
|
301
|
+
|
|
302
|
+
c0 = w1
|
|
303
|
+
c1 = dw1_dt * (t2 - t1)
|
|
304
|
+
c2 = -3 * c0 - 2 * c1
|
|
305
|
+
c3 = 2 * c0 + c1
|
|
306
|
+
wiggle_blend = c0 + c1 * u + c2 * u**2 + c3 * u**3
|
|
307
|
+
|
|
308
|
+
# Straight line
|
|
309
|
+
t_straight = np.linspace(t2, 1.0, 50)
|
|
310
|
+
wiggle_straight = np.zeros_like(t_straight)
|
|
311
|
+
|
|
312
|
+
t_all = np.concatenate([t_sine, t_blend[1:], t_straight[1:]])
|
|
313
|
+
wiggle_all = np.concatenate(
|
|
314
|
+
[wiggle_sine, wiggle_blend[1:], wiggle_straight[1:]]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
x_wiggle = x1 + dx * t_all + px * wiggle_all
|
|
318
|
+
y_wiggle = y1 + dy * t_all + py * wiggle_all
|
|
319
|
+
|
|
320
|
+
ax.plot(
|
|
321
|
+
x_wiggle,
|
|
322
|
+
y_wiggle,
|
|
323
|
+
color=color,
|
|
324
|
+
lw=linewidth,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Arrow head
|
|
328
|
+
ax.annotate(
|
|
329
|
+
"",
|
|
330
|
+
xy=(x_wiggle[-1], y_wiggle[-1]),
|
|
331
|
+
xytext=(x_wiggle[-5], y_wiggle[-5]),
|
|
332
|
+
arrowprops=dict(
|
|
333
|
+
arrowstyle="->",
|
|
334
|
+
color=color,
|
|
335
|
+
lw=linewidth,
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
ax.plot(
|
|
340
|
+
x_wiggle,
|
|
341
|
+
y_wiggle,
|
|
342
|
+
color=color,
|
|
343
|
+
lw=linewidth,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Arrow head (now perfectly tracking the final straight segment)
|
|
347
|
+
ax.annotate(
|
|
348
|
+
"",
|
|
349
|
+
xy=(x_wiggle[-1], y_wiggle[-1]),
|
|
350
|
+
xytext=(x_wiggle[-5], y_wiggle[-5]),
|
|
351
|
+
arrowprops=dict(
|
|
352
|
+
arrowstyle="->",
|
|
353
|
+
color=color,
|
|
354
|
+
lw=linewidth,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
if label:
|
|
358
|
+
xm = (x1 + x2) / 2
|
|
359
|
+
ym = (y1 + y2) / 2
|
|
360
|
+
ax.text(
|
|
361
|
+
xm,
|
|
362
|
+
ym,
|
|
363
|
+
label,
|
|
364
|
+
fontsize=10,
|
|
365
|
+
color=color,
|
|
366
|
+
bbox=dict(
|
|
367
|
+
facecolor="white",
|
|
368
|
+
edgecolor="none",
|
|
369
|
+
alpha=0.7,
|
|
370
|
+
),
|
|
371
|
+
)
|
jablonski/plots/plots.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
from typing import Callable, Iterable, Mapping
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pint
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
from matplotlib.collections import LineCollection
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
from poincare.printing.latex import Latex, ToLatex, default_packages, default_sections
|
|
11
|
+
from poincare.printing.latex import model_report as _model_report
|
|
12
|
+
|
|
13
|
+
from .._typing import Drawable, Pumper, RadiativeDecay
|
|
14
|
+
from .._units import ureg
|
|
15
|
+
from ..simulation import widened_emission_spectra
|
|
16
|
+
from ..states import SpectroscopicSystem, SpinState
|
|
17
|
+
from ..util import SpectraKind
|
|
18
|
+
from .jablonski_diagrams import JablonskiDiagram, Level, Number, Transition
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def graph_spectra(
|
|
22
|
+
system: SpectroscopicSystem,
|
|
23
|
+
excitation_transition: Pumper | Iterable[Pumper],
|
|
24
|
+
height: float,
|
|
25
|
+
unit: str | pint.Unit = ureg.nm,
|
|
26
|
+
kind: SpectraKind = "emission",
|
|
27
|
+
samples: Iterable[float] = np.linspace(380, 700, 1000),
|
|
28
|
+
width: float = 5, # TODO: what is the right width?
|
|
29
|
+
):
|
|
30
|
+
spectra = widened_emission_spectra(
|
|
31
|
+
system, excitation_transition, height, unit, kind, samples, width=width
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
points = spectra["wavelenght"].values
|
|
35
|
+
spectrum = spectra.values
|
|
36
|
+
plot_points = np.array([points, spectrum]).T.reshape(-1, 1, 2)
|
|
37
|
+
segments = np.concatenate([plot_points[:-1], plot_points[1:]], axis=1)
|
|
38
|
+
lc = LineCollection(segments, cmap="nipy_spectral")
|
|
39
|
+
lc.set_array(points)
|
|
40
|
+
fig, ax = plt.subplots()
|
|
41
|
+
ax.add_collection(lc)
|
|
42
|
+
ax.set_xlim(points.min(), points.max())
|
|
43
|
+
ax.set_ylim(spectrum.min(), spectrum.max())
|
|
44
|
+
ax.set_xlabel(f"Wavelenght [ {unit} ]")
|
|
45
|
+
ax.set_ylabel("Emission [ photons/s ]")
|
|
46
|
+
return fig, ax
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def jablonski_diagram(
|
|
50
|
+
system: SpectroscopicSystem,
|
|
51
|
+
figsize: tuple[Number, Number] = (6.4, 4.8),
|
|
52
|
+
fontsize: Number = 10,
|
|
53
|
+
show_energy_axis: bool = True,
|
|
54
|
+
unit: str | pint.Unit = ureg.eV,
|
|
55
|
+
) -> tuple[Axes, Figure]:
|
|
56
|
+
if isinstance(unit, str):
|
|
57
|
+
unit = ureg[unit]
|
|
58
|
+
levels = {
|
|
59
|
+
level: Level(
|
|
60
|
+
label=level.name,
|
|
61
|
+
energy=level.energy.to(unit).magnitude,
|
|
62
|
+
column=level.multiplicity,
|
|
63
|
+
)
|
|
64
|
+
for level in system._yield(SpinState)
|
|
65
|
+
}
|
|
66
|
+
transitions = [
|
|
67
|
+
Transition(
|
|
68
|
+
source=levels[transition._source],
|
|
69
|
+
target=levels[transition._target],
|
|
70
|
+
radiative=isinstance(transition, Pumper | RadiativeDecay),
|
|
71
|
+
)
|
|
72
|
+
for transition in system._yield(Drawable)
|
|
73
|
+
]
|
|
74
|
+
columns = []
|
|
75
|
+
has_singlet = any(level.column == "singlet" for level in levels.values())
|
|
76
|
+
has_triplet = any(level.column == "triplet" for level in levels.values())
|
|
77
|
+
if has_singlet:
|
|
78
|
+
columns.append("singlet")
|
|
79
|
+
if has_triplet:
|
|
80
|
+
columns.append("triplet")
|
|
81
|
+
jd = JablonskiDiagram(
|
|
82
|
+
levels=list(levels.values()),
|
|
83
|
+
transitions=transitions,
|
|
84
|
+
columns=columns,
|
|
85
|
+
)
|
|
86
|
+
fig, ax = jd.plot(
|
|
87
|
+
figsize=figsize,
|
|
88
|
+
fontsize=fontsize,
|
|
89
|
+
show_energy_axis=show_energy_axis,
|
|
90
|
+
)
|
|
91
|
+
ax.set_ylabel(f"Energy [{str(unit)} ]")
|
|
92
|
+
return fig, ax
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def jablonski_diagram_section(model: SpectroscopicSystem, latex: ToLatex):
|
|
96
|
+
backend = plt.get_backend()
|
|
97
|
+
plt.switch_backend("pgf")
|
|
98
|
+
fig, ax = jablonski_diagram(model, figsize=(6, 4))
|
|
99
|
+
|
|
100
|
+
with StringIO() as plot_buffer:
|
|
101
|
+
fig.savefig(plot_buffer, format="pgf")
|
|
102
|
+
plt.switch_backend(backend)
|
|
103
|
+
return (
|
|
104
|
+
"\\begin{figure}[H]\n\\centering\n"
|
|
105
|
+
+ plot_buffer.getvalue()
|
|
106
|
+
+ "\n\\end{figure}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def model_report(
|
|
111
|
+
model: type[SpectroscopicSystem],
|
|
112
|
+
path: str | None = None,
|
|
113
|
+
transform: dict | None = None,
|
|
114
|
+
descriptions: dict | None = None,
|
|
115
|
+
standalone: bool = True,
|
|
116
|
+
replace_algebraics: bool = False,
|
|
117
|
+
sections: Mapping[
|
|
118
|
+
str, Callable[[SpectroscopicSystem, ToLatex], str]
|
|
119
|
+
] = default_sections | {"Jablonski diagram": jablonski_diagram_section},
|
|
120
|
+
packages: Iterable[str] = default_packages + ["pgf"],
|
|
121
|
+
# packages: Iterable[str] = default_packages + ["graphicx", "inline-images"],
|
|
122
|
+
) -> Latex | None:
|
|
123
|
+
return _model_report(
|
|
124
|
+
model=model,
|
|
125
|
+
path=path,
|
|
126
|
+
descriptions=descriptions,
|
|
127
|
+
standalone=standalone,
|
|
128
|
+
replace_algebraics=replace_algebraics,
|
|
129
|
+
sections=sections,
|
|
130
|
+
packages=packages,
|
|
131
|
+
)
|