Skip to content

Commit 4fd7bc7

Browse files
committed
feat: Add atomic energy level visualization This commit introduces a visualization tool to plot energy level diagrams (Grotrian-like) for Ion instances from the atomic interface. The function generate_energy_level_plot takes an Ion object and produces a matplotlib figure showing its energy levels grouped by orbital quantum number and the transitions between them. Matplotlib has been added as a project dependency. Closes #58
1 parent 60eeb3c commit 4fd7bc7

File tree

4 files changed

+191
-2
lines changed

4 files changed

+191
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies = [
3838
"pydantic>=2.10.6",
3939
"oqd-compiler-infrastructure@git+https://github.com/openquantumdesign/oqd-compiler-infrastructure",
4040
"numpy>=2.2.3",
41+
"matplotlib",
4142
]
4243

4344
[project.optional-dependencies]

src/oqd_core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from . import backend, compiler, interface
15+
from . import backend, compiler, interface, visualizations
1616

17-
__all__ = ["interface", "compiler", "backend"]
17+
__all__ = ["interface", "compiler","backend" "visualizations"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2024-2025 Open Quantum Design
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .atomic import generate_energy_level_plot
16+
17+
__all__ = ["generate_energy_level_plot"]
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright 2024-2025 Open Quantum Design
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import matplotlib.pyplot as plt
16+
from matplotlib.figure import Figure
17+
import numpy as np
18+
from typing import Dict, List
19+
20+
# Actual imports from oqd_core
21+
from oqd_core.interface.atomic import Ion, Level, Transition
22+
23+
24+
def get_orbital_label(l_value: float) -> str:
25+
"""Converts orbital quantum number l to its spectroscopic letter."""
26+
# The Level.orbital is NonNegativeAngularMomentumNumber (float)
27+
# For labels, we typically use integer part if it's an integer
28+
l_int = int(l_value)
29+
if l_value == l_int:
30+
labels = {0: 's', 1: 'p', 2: 'd', 3: 'f', 4: 'g', 5: 'h'}
31+
return labels.get(l_int, f'l={l_int}')
32+
return f'l={l_value}' # For half-integer or other float cases if they occur
33+
34+
35+
def generate_energy_level_plot(ion: Ion, **plot_kwargs) -> Figure:
36+
"""
37+
Generates an energy level diagram (Grotrian-like diagram) for a given Ion instance.
38+
39+
Args:
40+
ion: An instance of the Ion class containing energy levels and transitions.
41+
**plot_kwargs: Additional keyword arguments to customize the plot.
42+
figure (dict): Arguments for plt.subplots (e.g., {'figsize': (10, 8)})
43+
level_color (str): Color for level lines.
44+
level_linewidth (float): Linewidth for level lines.
45+
level_fontsize (float): Fontsize for level labels.
46+
level_line_width_half (float): Half-width of the horizontal level lines.
47+
text_offset_x (float): Horizontal offset for level labels from line end.
48+
text_offset_y_factor (float): Factor to determine vertical offset for level labels (multiplied by energy range).
49+
show_n_quantum_number (bool): Whether to show principal quantum number in level labels.
50+
arrow_style (str): Matplotlib arrow style for transitions.
51+
transition_color (str): Color for transition arrows.
52+
arrow_shrink_a (float): Shrink factor for arrow tip.
53+
arrow_shrink_b (float): Shrink factor for arrow base.
54+
transition_linewidth (float): Linewidth for transition arrows.
55+
show_transition_labels (bool): Whether to show labels for transitions.
56+
transition_label_color (str): Color for transition labels.
57+
transition_label_fontsize (float): Fontsize for transition labels.
58+
xlabel (str): Label for the x-axis.
59+
ylabel (str): Label for the y-axis.
60+
title (str): Title for the plot.
61+
62+
Returns:
63+
A matplotlib.figure.Figure object representing the plot.
64+
"""
65+
fig, ax = plt.subplots(**plot_kwargs.get('figure', {}))
66+
67+
ion_name = getattr(ion, 'name', 'Ion') # Check if Ion has a name/label, else default
68+
69+
if not ion.levels:
70+
ax.text(0.5, 0.5, "No energy levels defined for this ion.",
71+
horizontalalignment='center', verticalalignment='center',
72+
transform=ax.transAxes)
73+
ax.set_title(plot_kwargs.get('title', f"Energy Level Diagram for {ion_name}"))
74+
return fig
75+
76+
# Create a mapping from level labels to Level objects for quick lookup if transitions use strings
77+
levels_map_by_label: Dict[str, Level] = {level.label: level for level in ion.levels}
78+
79+
# Group levels by orbital quantum number (l)
80+
levels_by_orbital: dict[float, List[Level]] = {}
81+
for level in ion.levels:
82+
orbital_val = float(level.orbital) # Ensure it's float for dict key
83+
if orbital_val not in levels_by_orbital:
84+
levels_by_orbital[orbital_val] = []
85+
levels_by_orbital[orbital_val].append(level)
86+
87+
sorted_orbitals = sorted(levels_by_orbital.keys())
88+
orbital_x_coords = {l_val: i for i, l_val in enumerate(sorted_orbitals)}
89+
90+
level_line_width_half = plot_kwargs.get("level_line_width_half", 0.35)
91+
text_offset_x = plot_kwargs.get("text_offset_x", 0.05)
92+
93+
level_coords: Dict[str, tuple[float, float]] = {}
94+
min_energy = float('inf')
95+
max_energy = float('-inf')
96+
97+
for level in ion.levels: # Iterate once to find min/max energy for dynamic text_offset_y
98+
min_energy = min(min_energy, level.energy)
99+
max_energy = max(max_energy, level.energy)
100+
101+
energy_span = max_energy - min_energy if max_energy > min_energy else 1.0
102+
text_offset_y = plot_kwargs.get("text_offset_y_factor", 0.01) * energy_span
103+
104+
105+
for l_val, levels_in_group in levels_by_orbital.items():
106+
x_center = orbital_x_coords[l_val]
107+
levels_in_group.sort(key=lambda lvl: lvl.energy)
108+
109+
for level in levels_in_group:
110+
y = level.energy
111+
112+
ax.hlines(y, x_center - level_line_width_half, x_center + level_line_width_half,
113+
colors=plot_kwargs.get('level_color', 'black'),
114+
linewidth=plot_kwargs.get('level_linewidth', 1.5))
115+
116+
level_coords[level.label] = (x_center, y)
117+
118+
label_text = f"{level.label}"
119+
if plot_kwargs.get("show_n_quantum_number", True):
120+
label_text = f"n={level.principal}, {label_text}"
121+
122+
ax.text(x_center + level_line_width_half + text_offset_x, y + text_offset_y, label_text,
123+
verticalalignment='bottom',
124+
fontsize=plot_kwargs.get('level_fontsize', 8))
125+
126+
for transition in ion.transitions:
127+
# Resolve level1 and level2 if they are strings
128+
level1_obj = transition.level1 if isinstance(transition.level1, Level) else levels_map_by_label.get(str(transition.level1))
129+
level2_obj = transition.level2 if isinstance(transition.level2, Level) else levels_map_by_label.get(str(transition.level2))
130+
131+
if level1_obj and level2_obj and level1_obj.label in level_coords and level2_obj.label in level_coords:
132+
x1, y1 = level_coords[level1_obj.label]
133+
x2, y2 = level_coords[level2_obj.label]
134+
135+
ax.annotate("",
136+
xy=(x2, y2), xycoords='data',
137+
xytext=(x1, y1), textcoords='data',
138+
arrowprops=dict(arrowstyle=plot_kwargs.get('arrow_style', "->"),
139+
connectionstyle=plot_kwargs.get('connectionstyle', "arc3,rad=0.1"),
140+
color=plot_kwargs.get('transition_color', 'gray'),
141+
shrinkA=plot_kwargs.get('arrow_shrink_a', 5),
142+
shrinkB=plot_kwargs.get('arrow_shrink_b', 5),
143+
linewidth=plot_kwargs.get('transition_linewidth', 1)),
144+
)
145+
if plot_kwargs.get("show_transition_labels", False) and transition.label:
146+
mid_x = (x1 + x2) / 2
147+
mid_y = (y1 + y2) / 2
148+
# Add a small offset if x1 and x2 are the same to avoid label overlap with vertical line
149+
label_offset_x = 0.1 if x1 == x2 else 0
150+
ax.text(mid_x + label_offset_x, mid_y, transition.label,
151+
color=plot_kwargs.get('transition_label_color', 'blue'),
152+
fontsize=plot_kwargs.get('transition_label_fontsize', 7),
153+
ha='center', va='center',
154+
bbox=plot_kwargs.get('transition_label_bbox', dict(facecolor='white', alpha=0.5, edgecolor='none', pad=0.1)))
155+
156+
ax.set_xlabel(plot_kwargs.get('xlabel', "Orbital Angular Momentum (l)"))
157+
ax.set_ylabel(plot_kwargs.get('ylabel', "Energy"))
158+
ax.set_title(plot_kwargs.get('title', f"Energy Level Diagram for {ion_name}"))
159+
160+
if sorted_orbitals:
161+
ax.set_xticks(list(orbital_x_coords.values()))
162+
ax.set_xticklabels([get_orbital_label(l) for l in sorted_orbitals])
163+
else:
164+
ax.set_xticks([])
165+
166+
if min_energy != float('inf') and max_energy != float('-inf'):
167+
padding = 0.1 * energy_span if energy_span > 0 else 1.0
168+
ax.set_ylim(min_energy - padding, max_energy + padding)
169+
170+
plt.tight_layout()
171+
return fig

0 commit comments

Comments
 (0)