jablonski 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: jablonski
3
+ Version: 0.1.0
4
+ Summary: Write and simulate ODE systems to describe the transitions in spectroscopic molecular systems.
5
+ Keywords:
6
+ Author: Tomas Di Napoli, Hernan E. Grecco
7
+ Author-email: Tomas Di Napoli <tomas.dina98@gmail.com>, Hernan E. Grecco <hernan.grecco@gmail.com>
8
+ License-Expression: BSD-3-Clause
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Programming Language :: Python :: 3
17
+ Requires-Dist: poincare>=1.1.1
18
+ Requires-Python: >=3.12
19
+ Project-URL: homepage, https://github.com/dyscolab/jablonski
20
+ Project-URL: issues, https://github.com/dyscolab/jablonski/issues
21
+ Project-URL: documentation, https://dyscolab.github.io/jablonski
@@ -0,0 +1,112 @@
1
+ [project]
2
+ name = "jablonski"
3
+ authors = [
4
+ { name = "Tomas Di Napoli", email = "tomas.dina98@gmail.com" },
5
+ { name = "Hernan E. Grecco", email = "hernan.grecco@gmail.com" },
6
+ ]
7
+ description = "Write and simulate ODE systems to describe the transitions in spectroscopic molecular systems."
8
+ version = "0.1.0"
9
+ requires-python = ">=3.12"
10
+ keywords = []
11
+ license = "BSD-3-Clause"
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python",
17
+ "Topic :: Software Development :: Libraries",
18
+ "Intended Audience :: Science/Research",
19
+ "Topic :: Scientific/Engineering",
20
+ "Programming Language :: Python :: 3",
21
+ ]
22
+ dependencies = ["poincare >= 1.1.1"]
23
+
24
+ [project.urls]
25
+ homepage = "https://github.com/dyscolab/jablonski"
26
+ issues = "https://github.com/dyscolab/jablonski/issues"
27
+ documentation = "https://dyscolab.github.io/jablonski"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.9,<0.10"]
31
+ build-backend = "uv_build"
32
+
33
+ [tool.pytest.ini_options]
34
+ addopts = ["--import-mode=importlib"]
35
+
36
+ [tool.ruff.format]
37
+ docstring-code-format = true
38
+
39
+ [tool.ruff.lint]
40
+ extend-select = ["I"]
41
+
42
+ [tool.pyright]
43
+ typeCheckingMode = "standard"
44
+ venv = "test"
45
+ venvPath = ".pixi/envs/"
46
+
47
+ [tool.pyrefly]
48
+ python-interpreter-path = ".pixi/envs/default/bin/python"
49
+ project-excludes = [".pixi", "**/__pycache__"]
50
+
51
+ [tool.ty.environment]
52
+ python = ".pixi/envs/default"
53
+
54
+ [tool.pixi.workspace]
55
+ channels = ["conda-forge"]
56
+ platforms = ["linux-64", "osx-arm64", "win-64"]
57
+ preview = ["pixi-build"]
58
+
59
+ [tool.pixi.package.build]
60
+ backend = { name = "pixi-build-python", version = "0.4.*" }
61
+ config = { ignore-pypi-mapping = false } # Enable automatic PyPI-to-conda mapping
62
+
63
+ [tool.pixi.package.host-dependencies]
64
+ uv-build = "*"
65
+
66
+
67
+ [tool.pixi.environments]
68
+ default = ["test", "py312"]
69
+ lint = { features = ["lint"], no-default-feature = true }
70
+ build = { features = ["build"], no-default-feature = true }
71
+ test-py312 = ["test", "py312"]
72
+ test-py313 = ["test", "py313"]
73
+ test-py314 = ["test", "py314"]
74
+
75
+ [tool.pixi.dependencies]
76
+ jablonski = { path = "." }
77
+
78
+ [tool.pixi.feature.lint.dependencies]
79
+ pre-commit = "*"
80
+ pre-commit-hooks = "*"
81
+ taplo = "*"
82
+ ruff = "*"
83
+ mdformat = "*"
84
+ mdformat-ruff = "*"
85
+ pyright = "*"
86
+
87
+ [tool.pixi.feature.lint.tasks]
88
+ pre-commit-install = "pre-commit install"
89
+ lint = "pre-commit run"
90
+
91
+ [tool.pixi.feature.build.dependencies]
92
+ uv = "*"
93
+ python = "*"
94
+
95
+
96
+ [tool.pixi.feature.build.tasks]
97
+ _build = "uv build"
98
+ _publish = "uv publish"
99
+
100
+
101
+ [tool.pixi.feature.test.dependencies]
102
+ matplotlib = "*"
103
+ pytest = "*"
104
+ pooch = "*"
105
+
106
+ [tool.pixi.feature.test.tasks]
107
+ test = "pytest --doctest-modules"
108
+
109
+ [tool.pixi.feature]
110
+ py312.dependencies = { python = "3.12.*" }
111
+ py313.dependencies = { python = "3.13.*" }
112
+ py314.dependencies = { python = "3.14.*" }
@@ -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
+ ]
@@ -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
@@ -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
+ )