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.
- jablonski-0.1.0/PKG-INFO +21 -0
- jablonski-0.1.0/pyproject.toml +112 -0
- jablonski-0.1.0/src/jablonski/__init__.py +28 -0
- jablonski-0.1.0/src/jablonski/_typing.py +35 -0
- jablonski-0.1.0/src/jablonski/_units.py +20 -0
- jablonski-0.1.0/src/jablonski/plots/jablonski_diagrams.py +371 -0
- jablonski-0.1.0/src/jablonski/plots/plots.py +131 -0
- jablonski-0.1.0/src/jablonski/simulation.py +318 -0
- jablonski-0.1.0/src/jablonski/states.py +111 -0
- jablonski-0.1.0/src/jablonski/sweeps.py +53 -0
- jablonski-0.1.0/src/jablonski/tests/test_simulation.py +177 -0
- jablonski-0.1.0/src/jablonski/tests/test_sweeps.py +42 -0
- jablonski-0.1.0/src/jablonski/tests/test_transitions.py +334 -0
- jablonski-0.1.0/src/jablonski/transitions.py +317 -0
- jablonski-0.1.0/src/jablonski/util.py +96 -0
jablonski-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
)
|