Skip to content

Commit 182eafb

Browse files
committed
Integrate WeasWidget for structure viewer
1 parent c096cda commit 182eafb

2 files changed

Lines changed: 289 additions & 0 deletions

File tree

aiidalab_widgets_base/weas.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import ase
2+
import ipywidgets as ipw
3+
import numpy as np
4+
import spglib
5+
import traitlets as tl
6+
from aiida.orm.nodes.data.structure import _get_dimensionality
7+
from weas_widget.base_widget import BaseWidget as _BaseWidget
8+
from weas_widget.utils import ASEAdapter
9+
10+
from aiidalab_widgets_base.utils import ase2spglib
11+
12+
13+
class BaseWidget(_BaseWidget):
14+
# this is not used but needed for compatibility
15+
_camera_orientation = tl.List()
16+
17+
18+
class WeasWidgetViewer(ipw.HBox):
19+
"""A structure viewer widget for AiiDAlab using WeasWidget."""
20+
21+
# For compatibility with AiiDAlab-Widget-Base StructureManagerWidget
22+
input_selection = tl.List(tl.Int(), allow_none=True)
23+
selection = tl.List(tl.Int())
24+
structure = tl.Instance(ase.Atoms, allow_none=True)
25+
26+
_CELL_LABELS = {
27+
1: ["length", "Å"],
28+
2: ["area", "Ų"],
29+
3: ["volume", "ų"],
30+
}
31+
32+
def __init__(self, **kwargs):
33+
self._viewer = BaseWidget(**kwargs)
34+
self._viewer.modelStyle = 1 # Set to "Ball and Stick" style
35+
tl.link((self, "selection"), (self._viewer, "selectedAtomsIndices"))
36+
super().__init__([self._viewer, self._cell_tab()])
37+
38+
# For compatibility with AiiDAlab-Widget-Base StructureManagerWidget
39+
@tl.observe("structure")
40+
def _observe_structure(self, change):
41+
if self.structure is not None:
42+
self._viewer.atoms = ASEAdapter.to_weas(self.structure)
43+
self.cell = self.structure.cell if self.structure else None
44+
45+
@tl.observe("structure")
46+
def _observe_cell(self, _=None):
47+
# Updtate the Cell and Periodicity.
48+
if self.structure and self.structure.cell:
49+
self._update_cell_tab()
50+
else:
51+
self._reset_cell_tab()
52+
53+
@tl.observe("selection")
54+
def _observe_selection(self, _=None):
55+
if self.structure and self.structure.cell:
56+
self._update_cell_tab()
57+
else:
58+
self._reset_cell_tab()
59+
60+
def _update_cell_tab(self):
61+
self.cell = self.structure.cell
62+
cell_array = self.cell.array
63+
lengths = self.cell.lengths()
64+
angles = self.cell.angles()
65+
66+
spglib_structure = ase2spglib(self.structure)
67+
symmetry_dataset = spglib.get_symmetry_dataset(
68+
spglib_structure, symprec=1e-5, angle_tolerance=1.0
69+
)
70+
# Calculate the volume of the cell using the function from orm.StructureData
71+
dimension_data = _get_dimensionality(self.structure.pbc, self.cell)
72+
# Determine the label and unit based on dimensionality
73+
cell_label = self._CELL_LABELS.get(dimension_data["dim"])
74+
if cell_label:
75+
cell_volume = (
76+
f"Cell {cell_label[0]}: {dimension_data['value']:.4f} ({cell_label[1]})"
77+
)
78+
else:
79+
cell_volume = "Cell volume: -"
80+
81+
self.cell_info.value = (
82+
"<div style='font-size: 13px; font-weight: 600; margin-bottom: 8px;'>"
83+
"Structure info"
84+
"</div>"
85+
"<div style='font-size: 13px; line-height: 1.4;'>"
86+
"<table style='width: 100%; border-collapse: collapse;'>"
87+
"<tr>"
88+
"<th style='text-align: left; padding: 4px 24px 4px 0; "
89+
"border-bottom: 1px solid #e0e0e0;'>Cell vectors (Å)</th>"
90+
"<th style='text-align: left; padding: 4px 12px; "
91+
"border-bottom: 1px solid #e0e0e0;'>Vector length (Å)</th>"
92+
"<th style='text-align: left; padding: 4px 0; "
93+
"border-bottom: 1px solid #e0e0e0;'>Angles (°)</th>"
94+
"</tr>"
95+
"<tr>"
96+
"<td style='padding: 6px 24px 2px 0;'>"
97+
f"<i><b>a</b></i>: {cell_array[0][0]:.4f} {cell_array[0][1]:.4f} {cell_array[0][2]:.4f}"
98+
"</td>"
99+
"<td style='padding: 6px 12px 2px 0;'>"
100+
f"|<i><b>a</b></i>|: {lengths[0]:.4f}"
101+
"</td>"
102+
"<td style='padding: 6px 0 2px 0;'>"
103+
f"&alpha;: {angles[0]:.4f}"
104+
"</td>"
105+
"</tr>"
106+
"<tr>"
107+
"<td style='padding: 2px 24px 2px 0;'>"
108+
f"<i><b>b</b></i>: {cell_array[1][0]:.4f} {cell_array[1][1]:.4f} {cell_array[1][2]:.4f}"
109+
"</td>"
110+
"<td style='padding: 2px 12px 2px 0;'>"
111+
f"|<i><b>b</b></i>|: {lengths[1]:.4f}"
112+
"</td>"
113+
"<td style='padding: 2px 0 2px 0;'>"
114+
f"&beta;: {angles[1]:.4f}"
115+
"</td>"
116+
"</tr>"
117+
"<tr>"
118+
"<td style='padding: 2px 24px 6px 0;'>"
119+
f"<i><b>c</b></i>: {cell_array[2][0]:.4f} {cell_array[2][1]:.4f} {cell_array[2][2]:.4f}"
120+
"</td>"
121+
"<td style='padding: 2px 12px 6px 0;'>"
122+
f"|<i><b>c</b></i>|: {lengths[2]:.4f}"
123+
"</td>"
124+
"<td style='padding: 2px 0 6px 0;'>"
125+
f"&gamma;: {angles[2]:.4f}"
126+
"</td>"
127+
"</tr>"
128+
"</table>"
129+
"<div style='margin-top: 8px; padding-top: 6px; "
130+
"border-top: 1px solid #e0e0e0;'>"
131+
"<div style='font-weight: 600; margin-bottom: 4px;'>"
132+
"Symmetry information"
133+
"</div>"
134+
"<div>"
135+
"Spacegroup: "
136+
f"{symmetry_dataset['international']} (No.{symmetry_dataset['number']})"
137+
"</div>"
138+
"<div>"
139+
f"Hall: {symmetry_dataset['hall']} (No.{symmetry_dataset['hall_number']})"
140+
"</div>"
141+
"<div>"
142+
f"Periodicity: {self._periodicity_label(self.structure.pbc)}"
143+
"</div>"
144+
"<div style='margin-top: 4px; font-weight: 600;'>"
145+
f"{cell_volume}"
146+
"</div>"
147+
"</div>"
148+
"<div style='margin-top: 10px; padding-top: 6px; "
149+
"border-top: 1px solid #e0e0e0;'>"
150+
"<div style='font-weight: 600; margin-bottom: 4px;'>"
151+
"Selection"
152+
"</div>"
153+
f"{self._selection_info_html()}"
154+
"</div>"
155+
"</div>"
156+
)
157+
158+
def _reset_cell_tab(self):
159+
self.cell_info.value = (
160+
"<div style='font-size: 13px; font-weight: 600; margin-bottom: 8px;'>"
161+
"Structure info"
162+
"</div>"
163+
"<div style='font-size: 13px; line-height: 1.4;'>"
164+
"<table style='width: 100%; border-collapse: collapse;'>"
165+
"<tr>"
166+
"<th style='text-align: left; padding: 4px 24px 4px 0; "
167+
"border-bottom: 1px solid #e0e0e0;'>Cell vectors (Å)</th>"
168+
"<th style='text-align: left; padding: 4px 12px; "
169+
"border-bottom: 1px solid #e0e0e0;'>Vector length (Å)</th>"
170+
"<th style='text-align: left; padding: 4px 0; "
171+
"border-bottom: 1px solid #e0e0e0;'>Angles (°)</th>"
172+
"</tr>"
173+
"<tr>"
174+
"<td style='padding: 6px 24px 2px 0;'><i><b>a</b></i>:</td>"
175+
"<td style='padding: 6px 12px 2px 0;'>|<i><b>a</b></i>|:</td>"
176+
"<td style='padding: 6px 0 2px 0;'>&alpha;:</td>"
177+
"</tr>"
178+
"<tr>"
179+
"<td style='padding: 2px 24px 2px 0;'><i><b>b</b></i>:</td>"
180+
"<td style='padding: 2px 12px 2px 0;'>|<i><b>b</b></i>|:</td>"
181+
"<td style='padding: 2px 0 2px 0;'>&beta;:</td>"
182+
"</tr>"
183+
"<tr>"
184+
"<td style='padding: 2px 24px 6px 0;'><i><b>c</b></i>:</td>"
185+
"<td style='padding: 2px 12px 6px 0;'>|<i><b>c</b></i>|:</td>"
186+
"<td style='padding: 2px 0 6px 0;'>&gamma;:</td>"
187+
"</tr>"
188+
"</table>"
189+
"<div style='margin-top: 8px; padding-top: 6px; "
190+
"border-top: 1px solid #e0e0e0;'>"
191+
"<div style='font-weight: 600; margin-bottom: 4px;'>"
192+
"Symmetry information"
193+
"</div>"
194+
"<div>Spacegroup:</div>"
195+
"<div>Hall:</div>"
196+
"<div>Periodicity:</div>"
197+
"<div style='margin-top: 4px; font-weight: 600;'>"
198+
"Cell volume: -"
199+
"</div>"
200+
"</div>"
201+
"<div style='margin-top: 10px; padding-top: 6px; "
202+
"border-top: 1px solid #e0e0e0;'>"
203+
"<div style='font-weight: 600; margin-bottom: 4px;'>"
204+
"Selection"
205+
"</div>"
206+
"<div>Selection: -</div>"
207+
"</div>"
208+
"</div>"
209+
)
210+
211+
def _cell_tab(self):
212+
self.cell_info = ipw.HTML()
213+
214+
self._observe_cell()
215+
216+
return ipw.VBox([self.cell_info])
217+
218+
def _selection_info_html(self):
219+
if not self.selection:
220+
return "<div>Atom: -</div>"
221+
222+
indices = list(self.selection)
223+
symbols = self.structure.get_chemical_symbols()
224+
225+
if len(indices) == 1:
226+
index = indices[0]
227+
symbol = symbols[index]
228+
position = self.structure.positions[index]
229+
return (
230+
f"<div>Atom: {symbol}</div>"
231+
f"<div>Position: {position[0]:.4f} "
232+
f"{position[1]:.4f} {position[2]:.4f} Å</div>"
233+
)
234+
235+
if len(indices) == 2:
236+
symbol_pair = f"{symbols[indices[0]]}, {symbols[indices[1]]}"
237+
distance = self.structure.get_distance(indices[0], indices[1], mic=True)
238+
return (
239+
f"<div>Atoms: {symbol_pair}</div><div>Distance: {distance:.4f} Å</div>"
240+
)
241+
242+
if len(indices) == 3:
243+
positions = self.structure.positions[indices]
244+
angles = self._triangle_angles(positions)
245+
return (
246+
f"<div>Atoms: {symbols[indices[0]]}, {symbols[indices[1]]}, {symbols[indices[2]]}</div>"
247+
"<div>Angles:</div>"
248+
f"<div>at 1: {angles[0]:.2f}°</div>"
249+
f"<div>at 2: {angles[1]:.2f}°</div>"
250+
f"<div>at 3: {angles[2]:.2f}°</div>"
251+
)
252+
253+
counts = {}
254+
ordered_symbols = []
255+
for index in indices:
256+
symbol = symbols[index]
257+
if symbol not in counts:
258+
ordered_symbols.append(symbol)
259+
counts[symbol] = 0
260+
counts[symbol] += 1
261+
262+
counts_text = ", ".join(
263+
f"{counts[symbol]} {symbol}" for symbol in ordered_symbols
264+
)
265+
return f"<div>Atoms: {counts_text}</div>"
266+
267+
@staticmethod
268+
def _periodicity_label(pbc):
269+
axes = [axis for axis, is_periodic in zip("xyz", pbc) if is_periodic]
270+
return "".join(axes) if axes else "-"
271+
272+
@staticmethod
273+
def _triangle_angles(positions):
274+
p0, p1, p2 = positions
275+
angle0 = WeasWidgetViewer._angle_between(p1 - p0, p2 - p0)
276+
angle1 = WeasWidgetViewer._angle_between(p0 - p1, p2 - p1)
277+
angle2 = WeasWidgetViewer._angle_between(p0 - p2, p1 - p2)
278+
return (angle0, angle1, angle2)
279+
280+
@staticmethod
281+
def _angle_between(vec1, vec2):
282+
norm1 = np.linalg.norm(vec1)
283+
norm2 = np.linalg.norm(vec2)
284+
if norm1 == 0 or norm2 == 0:
285+
return 0.0
286+
cos_theta = np.dot(vec1, vec2) / (norm1 * norm2)
287+
cos_theta = np.clip(cos_theta, -1.0, 1.0)
288+
return float(np.degrees(np.arccos(cos_theta)))

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ install_requires =
3535
vapory~=0.1.2
3636
pandas~=2.1
3737
ipython>=7.33,<9.0
38+
weas-widget~=2.0
3839
python_requires = >=3.9
3940
include_package_data = True
4041
zip_safe = False

0 commit comments

Comments
 (0)