ipyvasp 0.9.91__py2.py3-none-any.whl → 0.9.94__py2.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.
- ipyvasp/__init__.py +1 -1
- ipyvasp/_enplots.py +41 -45
- ipyvasp/_lattice.py +13 -4
- ipyvasp/_version.py +1 -1
- ipyvasp/bsdos.py +1 -0
- ipyvasp/core/plot_toolkit.py +3 -2
- ipyvasp/lattice.py +4 -4
- ipyvasp/widgets.py +735 -732
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.94.dist-info}/METADATA +6 -2
- ipyvasp-0.9.94.dist-info/RECORD +25 -0
- ipyvasp-0.9.91.dist-info/RECORD +0 -25
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.94.dist-info}/LICENSE +0 -0
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.94.dist-info}/WHEEL +0 -0
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.94.dist-info}/entry_points.txt +0 -0
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.94.dist-info}/top_level.txt +0 -0
ipyvasp/widgets.py
CHANGED
|
@@ -4,37 +4,36 @@ __all__ = [
|
|
|
4
4
|
"Files",
|
|
5
5
|
"PropsPicker",
|
|
6
6
|
"BandsWidget",
|
|
7
|
-
"
|
|
7
|
+
"KPathWidget",
|
|
8
8
|
]
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
import inspect, re
|
|
12
|
-
from time import time
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
from collections.abc import Iterable
|
|
15
14
|
from functools import partial
|
|
16
|
-
from pprint import pformat
|
|
17
15
|
|
|
18
16
|
# Widgets Imports
|
|
19
17
|
from IPython.display import display
|
|
20
|
-
import ipywidgets as ipw
|
|
21
18
|
from ipywidgets import (
|
|
22
19
|
Layout,
|
|
23
20
|
Button,
|
|
24
|
-
Box,
|
|
25
21
|
HBox,
|
|
26
22
|
VBox,
|
|
27
23
|
Dropdown,
|
|
28
24
|
Text,
|
|
29
|
-
Checkbox,
|
|
30
25
|
Stack,
|
|
31
26
|
SelectMultiple,
|
|
27
|
+
TagsInput,
|
|
32
28
|
)
|
|
33
29
|
|
|
34
30
|
# More imports
|
|
35
31
|
import numpy as np
|
|
36
32
|
import pandas as pd
|
|
33
|
+
import ipywidgets as ipw
|
|
34
|
+
import traitlets
|
|
37
35
|
import plotly.graph_objects as go
|
|
36
|
+
import einteract as ei
|
|
38
37
|
|
|
39
38
|
# Internal imports
|
|
40
39
|
from . import utils as gu
|
|
@@ -184,9 +183,9 @@ class Files:
|
|
|
184
183
|
"Apply a func(path) -> dict and create a dataframe."
|
|
185
184
|
return summarize(self._files,func, **kwargs)
|
|
186
185
|
|
|
187
|
-
def load_results(self):
|
|
188
|
-
"Load result.json files from these paths into a dataframe."
|
|
189
|
-
return load_results(self._files)
|
|
186
|
+
def load_results(self,exclude_keys=None):
|
|
187
|
+
"Load result.json files from these paths into a dataframe, with optionally excluding keys."
|
|
188
|
+
return load_results(self._files,exclude_keys=exclude_keys)
|
|
190
189
|
|
|
191
190
|
def input_info(self, *tags):
|
|
192
191
|
"Grab input information into a dataframe from POSCAR and INCAR. Provide INCAR tags (case-insinsitive) to select only few of them."
|
|
@@ -202,7 +201,7 @@ class Files:
|
|
|
202
201
|
lines = [(k,v) for k,v in lines if k in tags]
|
|
203
202
|
d = {k:v for k,v in lines if not k.startswith('#')}
|
|
204
203
|
d.update({k:len(v) for k,v in p.types.items()})
|
|
205
|
-
d.update(zip('
|
|
204
|
+
d.update(zip(['a','b','c','v','alpha','beta','gamma'], [*p.norms,p.volume,*p.angles]))
|
|
206
205
|
return d
|
|
207
206
|
|
|
208
207
|
return self.with_name('POSCAR').summarize(info, tags=tags)
|
|
@@ -219,6 +218,18 @@ class Files:
|
|
|
219
218
|
if old in dd.options:
|
|
220
219
|
dd.value = old
|
|
221
220
|
|
|
221
|
+
def to_dropdown(self,description='File'):
|
|
222
|
+
"""
|
|
223
|
+
Convert this instance to Dropdown. If there is only one file, adds an
|
|
224
|
+
empty option to make that file switchable.
|
|
225
|
+
Options of this dropdown are update on calling `Files.update` method."""
|
|
226
|
+
if hasattr(self,'_dd'):
|
|
227
|
+
return self._dd # already created
|
|
228
|
+
|
|
229
|
+
options = self._files if len(self._files) != 1 else ['', *self._files] # make single file work
|
|
230
|
+
self._dd = Dropdown(description=description, options=options)
|
|
231
|
+
return self._dd
|
|
232
|
+
|
|
222
233
|
def add(self, path_or_files, glob = '*', exclude=None, **kwargs):
|
|
223
234
|
"""Add more files or with a diffrent glob on top of exitsing files. Returns same instance.
|
|
224
235
|
Useful to add multiple globbed files into a single chained call.
|
|
@@ -231,190 +242,39 @@ class Files:
|
|
|
231
242
|
def _unique(self, *files_tuples):
|
|
232
243
|
return tuple(np.unique(np.hstack(files_tuples)))
|
|
233
244
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
panel_size = 25,
|
|
239
|
-
**kwargs):
|
|
240
|
-
"""
|
|
241
|
-
Interact with `func(path, *args, <free_widgets,optional>, **kwargs)` that takes selected Path as first argument. Returns a widget that saves attributes of the function call such as .f, .args, .kwargs.
|
|
242
|
-
`args` are widgets to be modified by func, such as plotly's FigureWidget.
|
|
243
|
-
Note that `free_widgets` can also be passed to function but does not run function when changed.
|
|
244
|
-
|
|
245
|
-
See docs of self.interact for more details on the parameters. kwargs are passed to ipywidgets.interactive to create controls.
|
|
246
|
-
|
|
247
|
-
>>> import plotly.graph_objects as go
|
|
248
|
-
>>> import ipyvasp as ipv
|
|
249
|
-
>>> fs = ipv.Files('.','**/POSCAR')
|
|
250
|
-
>>> def plot(path, fig, bl,plot_cell,eqv_sites):
|
|
251
|
-
>>> ipv.iplot2widget(ipv.POSCAR(path).iplot_lattice(
|
|
252
|
-
>>> bond_length=bl,plot_cell=plot_cell,eqv_sites=eqv_sites
|
|
253
|
-
>>> ),fig_widget=fig) # it keeps updating same view
|
|
254
|
-
>>> out = fs.interactive(plot, go.FigureWidget(),bl=(0,6,2),plot_cell=True, eqv_sites=True)
|
|
255
|
-
|
|
256
|
-
>>> out.f # function
|
|
257
|
-
>>> out.args # arguments
|
|
258
|
-
>>> out.kwargs # keyword arguments
|
|
259
|
-
>>> out.result # result of function call which is same as out.f(*out.args, **out.kwargs)
|
|
260
|
-
|
|
261
|
-
.. note::
|
|
262
|
-
If you don't need to interpret the result of the function call, you can use the @self.interact decorator instead.
|
|
263
|
-
"""
|
|
264
|
-
info = ipw.HTML().add_class("fprogess")
|
|
265
|
-
dd = Dropdown(description='File', options=['',*self._files]) # allows single file workable
|
|
266
|
-
|
|
267
|
-
def interact_func(fname, **kws):
|
|
268
|
-
if fname and str(fname) != '': # This would be None if no file is selected
|
|
269
|
-
info.value = _progress_svg
|
|
270
|
-
try:
|
|
271
|
-
start = time()
|
|
272
|
-
if 'free_widgets' in func.__code__.co_varnames:
|
|
273
|
-
kws['free_widgets'] = free_widgets # user allowed to pass this
|
|
274
|
-
names = ', '.join(func.__code__.co_varnames)
|
|
275
|
-
print(f"Running {func.__name__}({names})")
|
|
276
|
-
func(Path(fname).absolute(), *args, **kws) # Have Path object absolue if user changes directory
|
|
277
|
-
print(f"Finished in {time() - start:.3f} seconds.")
|
|
278
|
-
finally:
|
|
279
|
-
info.value = ""
|
|
280
|
-
|
|
281
|
-
out = ipw.interactive(interact_func, options, fname=dd, **kwargs)
|
|
282
|
-
out._dd = dd # save reference to dropdown
|
|
283
|
-
out.output_widget = out.children[-1] # save reference to output widget for other widgets to use
|
|
284
|
-
|
|
285
|
-
if options.get("manual", False):
|
|
286
|
-
out.interact_button = out.children[-2] # save reference to interact button for other widgets to use
|
|
287
|
-
|
|
288
|
-
output = out.children[-1] # get output widget
|
|
289
|
-
output.clear_output(wait=True) # clear output by waiting to avoid flickering, this is important
|
|
290
|
-
output.layout = Layout(
|
|
291
|
-
overflow="auto", max_height="100%", width="100%"
|
|
292
|
-
) # make output scrollable and avoid overflow
|
|
293
|
-
|
|
294
|
-
others = out.children[1:-1] # exclude files_dd and Output widget
|
|
295
|
-
if not isinstance(panel_size,int):
|
|
296
|
-
raise TypeError('panel_size should be integer in units of em')
|
|
245
|
+
@_sub_doc(ei.interactive)
|
|
246
|
+
def interactive(self, *funcs, auto_update=True, app_layout=None, grid_css={},**kwargs):
|
|
247
|
+
if 'file' in kwargs:
|
|
248
|
+
raise KeyError("file is a reserved keyword argument to select path to file!")
|
|
297
249
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
.files-interact > div:last-child {{width:calc(100% - {panel_size}em)}}
|
|
308
|
-
.files-interact .fprogess {{position:absolute !important; left:50%; top:50%; transform:translate(-50%,-50%); z-index:1}}
|
|
309
|
-
</style>"""
|
|
310
|
-
if others:
|
|
311
|
-
others = [ipw.HTML(f"<hr/>{_style}"), *others]
|
|
312
|
-
else:
|
|
313
|
-
others = [ipw.HTML(_style)]
|
|
314
|
-
|
|
315
|
-
if free_widgets and not isinstance(free_widgets, (list, tuple)):
|
|
316
|
-
raise TypeError("free_widgets must be a list or tuple of widgets.")
|
|
250
|
+
has_file_param = False
|
|
251
|
+
for func in funcs:
|
|
252
|
+
if not callable(func):
|
|
253
|
+
raise TypeError(f"Each item in *funcs should be callable, got {type(func)}")
|
|
254
|
+
params = [k for k,v in inspect.signature(func).parameters.items()]
|
|
255
|
+
for key in params:
|
|
256
|
+
if key == 'file':
|
|
257
|
+
has_file_param = True
|
|
258
|
+
break
|
|
317
259
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
raise TypeError(f'args can only contain a DOMWidget instance, got {type(w)}')
|
|
260
|
+
if funcs and not has_file_param: # may be no func yet, that is test below
|
|
261
|
+
raise KeyError("At least one of funcs should take 'file' as parameter, none got it!")
|
|
321
262
|
|
|
322
|
-
|
|
323
|
-
output.layout.max_height = "200px"
|
|
324
|
-
output.layout.min_height = "8em" # fisrt fix
|
|
325
|
-
out_collapser = Checkbox(description="Hide output widget", value=False)
|
|
326
|
-
|
|
327
|
-
def toggle_output(change):
|
|
328
|
-
if out_collapser.value:
|
|
329
|
-
output.layout.height = "0px" # dont use display = 'none' as it will clear widgets and wont show again
|
|
330
|
-
output.layout.min_height = "0px"
|
|
331
|
-
else:
|
|
332
|
-
output.layout.height = "auto"
|
|
333
|
-
output.layout.min_height = "8em"
|
|
334
|
-
|
|
335
|
-
out_collapser.observe(toggle_output, "value")
|
|
336
|
-
others.append(out_collapser)
|
|
337
|
-
|
|
338
|
-
# This should be below output collapser
|
|
339
|
-
others = [
|
|
340
|
-
*others,
|
|
341
|
-
ipw.HTML(f"<hr/>"),
|
|
342
|
-
*(free_widgets or []),
|
|
343
|
-
] # add hr to separate other controls
|
|
344
|
-
|
|
345
|
-
out.children = [
|
|
346
|
-
HBox([ # reset children to include new widgets
|
|
347
|
-
VBox([dd, VBox(others)]), # other widgets in box to make scrollable independent file selection
|
|
348
|
-
VBox([Box([output]), *args, info]), # output in box to make scrollable,
|
|
349
|
-
],layout=Layout(height=height, max_height=height),
|
|
350
|
-
).add_class("files-interact")
|
|
351
|
-
] # important for every widget separately
|
|
352
|
-
return out
|
|
263
|
+
return ei.interactive(*funcs,auto_update=auto_update, app_layout = app_layout, grid_css=grid_css, file = self.to_dropdown(), **kwargs)
|
|
353
264
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
box._interact = self.interactive(func, *args, **kwargs)
|
|
357
|
-
box.children = box._interact.children
|
|
358
|
-
box._files._dd = box._interact._dd
|
|
359
|
-
|
|
360
|
-
def interact(self, *args,
|
|
361
|
-
free_widgets=None,
|
|
362
|
-
options={"manual": False},
|
|
363
|
-
height="400px",
|
|
364
|
-
panel_size=25,
|
|
365
|
-
**kwargs,
|
|
366
|
-
):
|
|
367
|
-
"""Interact with a `func(path, *args, <free_widgets,optional>, **kwargs)`. `path` is passed from selected File.
|
|
368
|
-
A CSS class 'files-interact' is added to the final widget to let you style it.
|
|
369
|
-
|
|
370
|
-
Parameters
|
|
371
|
-
----------
|
|
372
|
-
args :
|
|
373
|
-
Any displayable widget can be passed. These are placed below the output widget of interact.
|
|
374
|
-
For example you can add plotly's FigureWidget that updates based on the selection, these are passed to function after path.
|
|
375
|
-
free_widgets : list/tuple
|
|
376
|
-
Default is None. If not None, these are assumed to be ipywidgets and are placed below the widgets created by kwargs.
|
|
377
|
-
These can be passed to the decorated function if added as arguemnt there like `func(..., free_widgets)`, but don't trigger execution.
|
|
378
|
-
options : dict
|
|
379
|
-
Default is {'manual':False}. If True, the decorated function is not called automatically, and you have to call it manually on button press. You can pass button name as 'manual_name' in options.
|
|
380
|
-
height : str
|
|
381
|
-
Default is '90vh'. height of the final widget. This is important to avoid very long widgets.
|
|
382
|
-
panel_size: int
|
|
383
|
-
Side panel size in units of em.
|
|
384
|
-
|
|
385
|
-
kwargs are passed to ipywidgets.interactive and decorated function. Resulting widgets are placed below the file selection widget.
|
|
386
|
-
Widgets in `args` can be controlled by `free_widgets` externally if defined gloablly or inside function if you pass `free_widgets` as argument like `func(..., free_widgets)`.
|
|
387
|
-
|
|
388
|
-
The decorated function can be called later separately as well, and has .args and .kwargs attributes to access the latest arguments
|
|
389
|
-
and .result method to access latest. For a function `f`, `f.result` is same as `f(*f.args, **f.kwargs)`.
|
|
390
|
-
|
|
391
|
-
>>> import plotly.graph_objects as go
|
|
392
|
-
>>> import ipyvasp as ipv
|
|
393
|
-
>>> fs = ipv.Files('.','**/POSCAR')
|
|
394
|
-
>>> @fs.interact(go.FigureWidget(),bl=(0,6,2),plot_cell=True, eqv_sites=True)
|
|
395
|
-
>>> def plot(path, fig, bl,plot_cell,eqv_sites):
|
|
396
|
-
>>> ipv.iplot2widget(ipv.POSCAR(path).iplot_lattice(
|
|
397
|
-
>>> bond_length=bl,plot_cell=plot_cell,eqv_sites=eqv_sites
|
|
398
|
-
>>> ),fig_widget=fig) # it keeps updating same view
|
|
399
|
-
|
|
400
|
-
.. note::
|
|
401
|
-
Use self.interactive to get a widget that stores the argements and can be called later in a notebook cell.
|
|
402
|
-
"""
|
|
403
|
-
|
|
265
|
+
@_sub_doc(ei.interact)
|
|
266
|
+
def interact(self, *funcs, auto_update=True, app_layout=None, grid_css={},**kwargs):
|
|
404
267
|
def inner(func):
|
|
405
|
-
display(self.interactive(func, *
|
|
406
|
-
|
|
407
|
-
options=options,
|
|
408
|
-
height=height,
|
|
409
|
-
panel_size=panel_size,
|
|
268
|
+
display(self.interactive(func, *funcs,
|
|
269
|
+
auto_update=auto_update, app_layout = app_layout, grid_css=grid_css,
|
|
410
270
|
**kwargs)
|
|
411
271
|
)
|
|
412
272
|
return func
|
|
413
273
|
return inner
|
|
414
274
|
|
|
415
275
|
def kpath_widget(self, height='400px'):
|
|
416
|
-
"Get
|
|
417
|
-
return
|
|
276
|
+
"Get KPathWidget instance with these files."
|
|
277
|
+
return KPathWidget(files = self.with_name('POSCAR'), height = height)
|
|
418
278
|
|
|
419
279
|
def bands_widget(self, height='450px'):
|
|
420
280
|
"Get BandsWidget instance with these files."
|
|
@@ -471,84 +331,109 @@ class Files:
|
|
|
471
331
|
|
|
472
332
|
@fix_signature
|
|
473
333
|
class _PropPicker(VBox):
|
|
334
|
+
"""Single projection picker with atoms and orbitals selection"""
|
|
335
|
+
props = traitlets.Dict({})
|
|
336
|
+
|
|
474
337
|
def __init__(self, system_summary=None):
|
|
475
338
|
super().__init__()
|
|
476
|
-
self.
|
|
477
|
-
"atoms"
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
self.
|
|
487
|
-
|
|
488
|
-
self._atoms = {}
|
|
489
|
-
self._orbs = {}
|
|
339
|
+
self._atoms = TagsInput(description="Atoms", allowed_tags=[],
|
|
340
|
+
placeholder="Select atoms", allow_duplicates = False).add_class('props-tags')
|
|
341
|
+
self._orbs = TagsInput(description="Orbs", allowed_tags=[],
|
|
342
|
+
placeholder="Select orbitals", allow_duplicates = False).add_class('props-tags')
|
|
343
|
+
self.children = [self._atoms, self._orbs]
|
|
344
|
+
self.layout.width = '100%' # avoid horizontal collapse
|
|
345
|
+
self._atoms_map = {}
|
|
346
|
+
self._orbs_map = {}
|
|
347
|
+
|
|
348
|
+
# Link changes
|
|
349
|
+
self._atoms.observe(self._update_props, 'value')
|
|
350
|
+
self._orbs.observe(self._update_props, 'value')
|
|
490
351
|
self._process(system_summary)
|
|
491
352
|
|
|
353
|
+
def _update_props(self, change):
|
|
354
|
+
"""Update props trait when selections change"""
|
|
355
|
+
_atoms = [self._atoms_map.get(tag, None) for tag in self._atoms.value]
|
|
356
|
+
_orbs = [self._orbs_map.get(tag, None) for tag in self._orbs.value]
|
|
357
|
+
|
|
358
|
+
# Filter out None values, and flatten
|
|
359
|
+
# Flatten and filter atoms
|
|
360
|
+
atoms = []
|
|
361
|
+
for ats in _atoms:
|
|
362
|
+
atoms.extend(ats if ats is not None else [])
|
|
363
|
+
|
|
364
|
+
# Flatten and filter orbitals
|
|
365
|
+
orbs = []
|
|
366
|
+
for ors in _orbs:
|
|
367
|
+
orbs.extend(ors if ors is not None else [])
|
|
368
|
+
|
|
369
|
+
if atoms and orbs:
|
|
370
|
+
self.props = {
|
|
371
|
+
'atoms': atoms, 'orbs': orbs,
|
|
372
|
+
'label': f"{'+'.join(self._atoms.value)} | {'+'.join(self._orbs.value)}"
|
|
373
|
+
}
|
|
374
|
+
else:
|
|
375
|
+
self.props = {}
|
|
376
|
+
|
|
492
377
|
def _process(self, system_summary):
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
]
|
|
497
|
-
return None
|
|
378
|
+
"""Process system data and setup widget options"""
|
|
379
|
+
if system_summary is None or not hasattr(system_summary, "orbs"):
|
|
380
|
+
return
|
|
498
381
|
|
|
499
|
-
self.children = [self._widgets["atoms"], self._widgets["orbs"]]
|
|
500
382
|
sorbs = system_summary.orbs
|
|
383
|
+
self._orbs_map = {"All": range(len(sorbs)), "s": [0]}
|
|
501
384
|
|
|
502
|
-
|
|
385
|
+
# p-orbitals
|
|
503
386
|
if set(["px", "py", "pz"]).issubset(sorbs):
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
idx for idx, key in enumerate(sorbs) if key in ("px", "py")
|
|
507
|
-
|
|
508
|
-
|
|
387
|
+
self._orbs_map.update({
|
|
388
|
+
"p": range(1, 4),
|
|
389
|
+
"px+py": [idx for idx, key in enumerate(sorbs) if key in ("px", "py")],
|
|
390
|
+
**{k: [v] for k, v in zip(sorbs[1:4], range(1, 4))}
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
# d-orbitals
|
|
509
394
|
if set(["dxy", "dyz"]).issubset(sorbs):
|
|
510
|
-
|
|
511
|
-
|
|
395
|
+
self._orbs_map.update({
|
|
396
|
+
"d": range(4, 9),
|
|
397
|
+
**{k: [v] for k, v in zip(sorbs[4:9], range(4, 9))}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
# f-orbitals
|
|
512
401
|
if len(sorbs) == 16:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
402
|
+
self._orbs_map.update({
|
|
403
|
+
"f": range(9, 16),
|
|
404
|
+
**{k: [v] for k, v in zip(sorbs[9:16], range(9, 16))}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
# Extra orbitals beyond f
|
|
408
|
+
if len(sorbs) > 16:
|
|
409
|
+
self._orbs_map.update({
|
|
410
|
+
k: [idx] for idx, k in enumerate(sorbs[16:], start=16)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
self._orbs.allowed_tags = list(self._orbs_map.keys())
|
|
414
|
+
|
|
415
|
+
# Process atoms
|
|
416
|
+
self._atoms_map = {
|
|
417
|
+
"All": range(system_summary.NIONS),
|
|
418
|
+
**{k: v for k,v in system_summary.types.to_dict().items()},
|
|
419
|
+
**{f"{k}{n}": [v] for k,tp in system_summary.types.to_dict().items()
|
|
420
|
+
for n,v in enumerate(tp, 1)}
|
|
421
|
+
}
|
|
422
|
+
self._atoms.allowed_tags = list(self._atoms_map.keys())
|
|
423
|
+
self._update_props(None) # Trigger props update
|
|
535
424
|
|
|
536
425
|
def update(self, system_summary):
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"label"
|
|
546
|
-
] = f"{self._widgets['atoms'].value or ''}-{self._widgets['orbs'].value or ''}"
|
|
547
|
-
return items
|
|
548
|
-
|
|
426
|
+
"""Update widget with new system data while preserving selections"""
|
|
427
|
+
old_atoms = self._atoms.value
|
|
428
|
+
old_orbs = self._orbs.value
|
|
429
|
+
self._process(system_summary)
|
|
430
|
+
|
|
431
|
+
# Restore previous selections if still valid
|
|
432
|
+
self._atoms.value = [tag for tag in old_atoms if tag in self._atoms.allowed_tags]
|
|
433
|
+
self._orbs.value = [tag for tag in old_orbs if tag in self._orbs.allowed_tags]
|
|
549
434
|
|
|
550
435
|
@fix_signature
|
|
551
|
-
class PropsPicker(VBox):
|
|
436
|
+
class PropsPicker(VBox): # NOTE: remove New Later
|
|
552
437
|
"""
|
|
553
438
|
A widget to pick atoms and orbitals for plotting.
|
|
554
439
|
|
|
@@ -556,91 +441,87 @@ class PropsPicker(VBox):
|
|
|
556
441
|
----------
|
|
557
442
|
system_summary : (Vasprun,Vaspout).summary
|
|
558
443
|
N : int, default is 3, number of projections to pick.
|
|
559
|
-
|
|
560
|
-
|
|
444
|
+
|
|
445
|
+
You can observe `projections` trait.
|
|
561
446
|
"""
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
):
|
|
447
|
+
projections = traitlets.Dict({})
|
|
448
|
+
|
|
449
|
+
def __init__(self, system_summary=None, N=3):
|
|
566
450
|
super().__init__()
|
|
567
|
-
self.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
selected_index=0,
|
|
451
|
+
self._N = N
|
|
452
|
+
self._pickers = [_PropPicker(system_summary) for _ in range(N)]
|
|
453
|
+
self.add_class("props-picker")
|
|
454
|
+
|
|
455
|
+
# Create widgets with consistent width
|
|
456
|
+
self._picker = Dropdown(
|
|
457
|
+
description="Color" if N == 3 else "Projection",
|
|
458
|
+
options=["Red", "Green", "Blue"] if N == 3 else [str(i+1) for i in range(N)],
|
|
576
459
|
)
|
|
577
|
-
self.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
460
|
+
self._stack = Stack(children=self._pickers, selected_index=0)
|
|
461
|
+
# Link picker dropdown to stack
|
|
462
|
+
ipw.link((self._picker, 'index'), (self._stack, 'selected_index'))
|
|
463
|
+
|
|
464
|
+
# Setup layout
|
|
465
|
+
self.children = [self._picker, self._stack]
|
|
466
|
+
|
|
467
|
+
# Observe pickers for props changes and button click
|
|
468
|
+
for picker in self._pickers:
|
|
469
|
+
picker.observe(self._update_projections, names=['props'])
|
|
470
|
+
|
|
471
|
+
def _update_projections(self, change):
|
|
472
|
+
"""Update combined projections when any picker changes"""
|
|
473
|
+
projs = {}
|
|
474
|
+
for picker in self._pickers:
|
|
475
|
+
if picker.props: # Only add non-empty selections
|
|
476
|
+
projs[picker.props['label']] = (
|
|
477
|
+
picker.props['atoms'],
|
|
478
|
+
picker.props['orbs']
|
|
479
|
+
)
|
|
480
|
+
self.projections = projs
|
|
481
|
+
|
|
592
482
|
def update(self, system_summary):
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if not
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
def store_selected_data(fig, store_dict, callback=None):
|
|
638
|
-
"Store multipoints selected data to a store_dict. callback takes trace being clicked as argument."
|
|
639
|
-
return __store_figclick_data(fig, store_dict, callback, selection=True)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
def load_results(paths_list):
|
|
643
|
-
"Loads result.json from paths_list and returns a dataframe."
|
|
483
|
+
"""Update all pickers with new system data"""
|
|
484
|
+
for picker in self._pickers:
|
|
485
|
+
picker.update(system_summary)
|
|
486
|
+
|
|
487
|
+
def _clean_legacy_data(path):
|
|
488
|
+
"clean old style keys like VBM to vbm"
|
|
489
|
+
data = serializer.load(path.absolute()) # Old data loaded
|
|
490
|
+
if not any(key in data for key in ['VBM', 'α','vbm_k']):
|
|
491
|
+
return data # already clean
|
|
492
|
+
|
|
493
|
+
keys_map = {
|
|
494
|
+
"SYSTEM": "sys",
|
|
495
|
+
"VBM": "vbm", # Old: New
|
|
496
|
+
"CBM": "cbm",
|
|
497
|
+
"VBM_k": "kvbm", "vbm_k": "kvbm",
|
|
498
|
+
"CBM_k": "kcbm", "cbm_k": "kcbm",
|
|
499
|
+
"E_gap": "gap",
|
|
500
|
+
"\u0394_SO": "soc",
|
|
501
|
+
"α": "alpha",
|
|
502
|
+
"β": "beta",
|
|
503
|
+
"γ": "gamma",
|
|
504
|
+
}
|
|
505
|
+
new_data = {k:v for k,v in data.items() if k not in (*keys_map.keys(),*keys_map.values())} # keep other data
|
|
506
|
+
for old, new in keys_map.items():
|
|
507
|
+
if old in data:
|
|
508
|
+
new_data[new] = data[old] # Transfer value from old key to new key
|
|
509
|
+
elif new in data:
|
|
510
|
+
new_data[new] = data[new] # Keep existing new style keys
|
|
511
|
+
|
|
512
|
+
# save cleaned data
|
|
513
|
+
serializer.dump(new_data,format="json",outfile=path)
|
|
514
|
+
return new_data
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def load_results(paths_list, exclude_keys=None):
|
|
518
|
+
"Loads result.json from paths_list and returns a dataframe. Use exclude_keys to get subset of data."
|
|
519
|
+
if exclude_keys is not None:
|
|
520
|
+
if not isinstance(exclude_keys, (list,tuple)):
|
|
521
|
+
raise TypeError(f"exclude_keys should be list of keys, got {type(exclude_keys)}")
|
|
522
|
+
if not all([isinstance(key,str) for key in exclude_keys]):
|
|
523
|
+
raise TypeError(f"all keys in exclude_keys should be str!")
|
|
524
|
+
|
|
644
525
|
paths_list = [Path(p) for p in paths_list]
|
|
645
526
|
result_paths = []
|
|
646
527
|
if paths_list:
|
|
@@ -652,217 +533,339 @@ def load_results(paths_list):
|
|
|
652
533
|
|
|
653
534
|
def load_data(path):
|
|
654
535
|
try:
|
|
655
|
-
|
|
536
|
+
data = _clean_legacy_data(path)
|
|
537
|
+
return {k:v for k,v in data.items() if k not in (exclude_keys or [])}
|
|
656
538
|
except:
|
|
657
539
|
return {} # If not found, return empty dictionary
|
|
658
540
|
|
|
659
541
|
return summarize(result_paths, load_data)
|
|
660
542
|
|
|
543
|
+
def _get_css(mode):
|
|
544
|
+
return {
|
|
545
|
+
'--jp-widgets-color': 'white' if mode == 'dark' else 'black',
|
|
546
|
+
'--jp-widgets-label-color': 'white' if mode == 'dark' else 'black',
|
|
547
|
+
'--jp-widgets-readout-color': 'white' if mode == 'dark' else 'black',
|
|
548
|
+
'--jp-widgets-input-color': 'white' if mode == 'dark' else 'black',
|
|
549
|
+
'--jp-widgets-input-background-color': '#222' if mode == 'dark' else '#f7f7f7',
|
|
550
|
+
'--jp-widgets-input-border-color': '#8988' if mode == 'dark' else '#ccc',
|
|
551
|
+
'--jp-layout-color2': '#555' if mode == 'dark' else '#ddd', # buttons
|
|
552
|
+
'--jp-ui-font-color1': 'whitesmoke' if mode == 'dark' else 'black', # buttons
|
|
553
|
+
'--jp-content-font-color1': 'white' if mode == 'dark' else 'black', # main text
|
|
554
|
+
'--jp-layout-color1': '#111' if mode == 'dark' else '#fff', # background
|
|
555
|
+
':fullscreen': {'min-height':'100vh'},
|
|
556
|
+
'background': 'var(--jp-widgets-input-background-color)', 'border-radius': '4px', 'padding':'4px 4px 0 4px',
|
|
557
|
+
'> *': {
|
|
558
|
+
'box-sizing': 'border-box',
|
|
559
|
+
'background': 'var(--jp-layout-color1)',
|
|
560
|
+
'border-radius': '4px', 'grid-gap': '8px', 'padding': '8px',
|
|
561
|
+
},
|
|
562
|
+
'.left-sidebar .sm': {
|
|
563
|
+
'flex-grow': 1,
|
|
564
|
+
'select': {'height': '100%',},
|
|
565
|
+
},
|
|
566
|
+
'.footer': {'overflow': 'auto','padding':0},
|
|
567
|
+
'.widget-vslider, .jupyter-widget-vslider': {'width': 'auto'}, # otherwise it spans too much area
|
|
568
|
+
'table': { # dataframe display sucks
|
|
569
|
+
'color':'var(--jp-content-font-color1)',
|
|
570
|
+
'background':'var(--jp-layout-color1)',
|
|
571
|
+
'tr': {
|
|
572
|
+
'^:nth-child(odd)': {'background':'var(--jp-widgets-input-background-color)',},
|
|
573
|
+
'^:nth-child(even)': {'background':'var(--jp-layout-color1)',},
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
'.props-picker': {
|
|
577
|
+
'background': 'var(--jp-widgets-input-background-color)', # make feels like single widget
|
|
578
|
+
'overflow-x': 'hidden', 'border-radius': '4px', 'padding': '4px',
|
|
579
|
+
},
|
|
580
|
+
'.props-tags': {
|
|
581
|
+
'background':'var(--jp-layout-color1)', 'border-radius': '4px', 'padding': '4px',
|
|
582
|
+
'> input': {'width': '100%'},
|
|
583
|
+
'> input::placeholder': {'color': 'var(--jp-ui-font-color1)'},
|
|
584
|
+
},
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
class _ThemedFigureInteract(ei.InteractBase):
|
|
588
|
+
"Keeps self._fig anf self._theme button attributes for subclasses to use."
|
|
589
|
+
def __init__(self, *args, **kwargs):
|
|
590
|
+
self._fig = ei.patched_plotly(go.FigureWidget())
|
|
591
|
+
self._theme = Button(icon='sun', description=' ', tooltip="Toggle Theme")
|
|
592
|
+
super().__init__(*args, **kwargs)
|
|
593
|
+
|
|
594
|
+
if not all([hasattr(self.params, 'fig'), hasattr(self.params, 'theme')]):
|
|
595
|
+
raise AttributeError("subclass must include already initialized "
|
|
596
|
+
"{'fig': self._fig,'theme':self._theme} in returned dict of _interactive_params() method.")
|
|
597
|
+
self._update_theme(self._fig,self._theme) # fix theme in starts
|
|
598
|
+
|
|
599
|
+
def _interactive_params(self): return {}
|
|
600
|
+
|
|
601
|
+
def __init_subclass__(cls):
|
|
602
|
+
if (not '_update_theme' in cls.__dict__) or (not hasattr(cls._update_theme,'_is_interactive_callback')):
|
|
603
|
+
raise AttributeError("implement _update_theme(self, fig, theme) decorated by @callback in subclass, "
|
|
604
|
+
"which should only call super()._update_theme(fig, theme) in its body.")
|
|
605
|
+
super().__init_subclass__()
|
|
606
|
+
|
|
607
|
+
@ei.callback
|
|
608
|
+
def _update_theme(self, fig, theme):
|
|
609
|
+
require_dark = (theme.icon == 'sun')
|
|
610
|
+
theme.icon = 'moon' if require_dark else 'sun' # we are not observing icon, so we can do this
|
|
611
|
+
fig.layout.template = "plotly_dark" if require_dark else "plotly_white"
|
|
612
|
+
self.set_css() # automatically sets dark/light, ensure after icon set
|
|
613
|
+
fig.layout.autosize = True # must
|
|
614
|
+
|
|
615
|
+
@_sub_doc(ei.InteractBase.set_css) # overriding to alway be able to set_css
|
|
616
|
+
def set_css(self, main=None, center=None):
|
|
617
|
+
# This is after setting icon above, so logic is fliipped
|
|
618
|
+
style = _get_css("light" if self._theme.icon == 'sun' else 'dark') # infer from icon to match
|
|
619
|
+
if isinstance(main, dict):
|
|
620
|
+
style = {**style, **main} # main should allow override
|
|
621
|
+
elif main is not None:
|
|
622
|
+
raise TypeError("main must be a dict or None, got: {}".format(type(main)))
|
|
623
|
+
super().set_css(style, center)
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def files(self):
|
|
627
|
+
"Use self.files.update(...) to keep state of widget preserved with new files."
|
|
628
|
+
if not hasattr(self, '_files'): # subclasses must set this, although no check unless user dots it
|
|
629
|
+
raise AttributeError("self._files = Files(...) was never set!")
|
|
630
|
+
return self._files
|
|
631
|
+
|
|
661
632
|
|
|
662
633
|
@fix_signature
|
|
663
|
-
class BandsWidget(
|
|
634
|
+
class BandsWidget(_ThemedFigureInteract):
|
|
664
635
|
"""Visualize band structure from VASP calculation. You can click on the graph to get the data such as VBM, CBM, etc.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
self.selected_data returns the last selection of points within a box or lasso. You can plot that output separately as plt.plot(data.xs, data.ys) after a selection.
|
|
668
|
-
You can use `self.files.update` method to change source files without effecting state of widget.
|
|
669
|
-
"""
|
|
636
|
+
|
|
637
|
+
You can observe three traits:
|
|
670
638
|
|
|
671
|
-
|
|
672
|
-
|
|
639
|
+
- file: Currently selected file
|
|
640
|
+
- clicked_data: Last clicked point data, which can be directly passed to a dataframe.
|
|
641
|
+
- selected_data: Last selection of points within a box or lasso, which can be directly passed to a dataframe and plotted accordingly.
|
|
642
|
+
|
|
643
|
+
- You can use `self.files.update` method to change source files without effecting state of widget.
|
|
644
|
+
- You can also use `self.iplot`, `self.splot` with `self.kws` to get static plts of current state, and self.results to get a dataframe.
|
|
645
|
+
- You can use store_clicks to provide extra names of points you want to click and save data, besides default ones.
|
|
646
|
+
"""
|
|
647
|
+
file = traitlets.Any(allow_none=True)
|
|
648
|
+
clicked_data = traitlets.Dict(allow_none=True)
|
|
649
|
+
selected_data = traitlets.Dict(allow_none=True)
|
|
650
|
+
|
|
651
|
+
def __init__(self, files, height="600px", store_clicks=None):
|
|
652
|
+
self.add_class("BandsWidget")
|
|
653
|
+
self._kb_fig = go.FigureWidget() # for extra stuff
|
|
654
|
+
self._kb_fig.update_layout(margin=dict(l=40, r=0, b=40, t=40, pad=0)) # show compact
|
|
655
|
+
self._files = Files(files)
|
|
673
656
|
self._bands = None
|
|
674
|
-
self.
|
|
675
|
-
self.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
657
|
+
self._kws = {}
|
|
658
|
+
self._result = {}
|
|
659
|
+
self._extra_clicks = ()
|
|
660
|
+
|
|
661
|
+
if store_clicks is not None:
|
|
662
|
+
if not isinstance(store_clicks, (list,tuple)):
|
|
663
|
+
raise TypeError("store_clicks should be list of names "
|
|
664
|
+
f"of point to be stored from click on figure, got {type(store_clicks)}")
|
|
665
|
+
|
|
666
|
+
for name in store_clicks:
|
|
667
|
+
if not isinstance(name, str) or not name.isidentifier():
|
|
668
|
+
raise ValueError(f"items in store_clicks should be a valid python variable name, got {name!r}")
|
|
669
|
+
if name in ["vbm", "cbm", "so_max", "so_min"]:
|
|
670
|
+
raise ValueError(f"{name!r} already exists in default click points!")
|
|
671
|
+
reserved = "gap soc v a b c alpha beta gamma direct".split()
|
|
672
|
+
if name in reserved:
|
|
673
|
+
raise ValueError(f"{name!r} conflicts with reserved keys {reserved}")
|
|
674
|
+
|
|
675
|
+
self._extra_clicks += tuple(store_clicks)
|
|
676
|
+
|
|
677
|
+
super().__init__() # after extra clicks
|
|
678
|
+
|
|
679
|
+
traitlets.dlink((self.params.file,'value'),(self, 'file'))
|
|
680
|
+
traitlets.dlink((self.params.fig,'clicked'),(self, 'clicked_data'))
|
|
681
|
+
traitlets.dlink((self.params.fig,'selected'),(self, 'selected_data'))
|
|
690
682
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
self._krange,
|
|
696
|
-
self._ktcicks,
|
|
697
|
-
ipw.HTML("<hr/>"),
|
|
698
|
-
self._ppicks,
|
|
699
|
-
ipw.HTML("<hr/>Click on graph to read selected option."),
|
|
700
|
-
self._click,
|
|
683
|
+
self.relayout(
|
|
684
|
+
left_sidebar=[
|
|
685
|
+
'head','file','krange','kticks','brange', 'ppicks',
|
|
686
|
+
[HBox(),('theme','button')], 'kb_fig',
|
|
701
687
|
],
|
|
702
|
-
|
|
688
|
+
center=['hdata','fig','cpoint'], footer = self.groups.outputs,
|
|
689
|
+
right_sidebar = ['showft'],
|
|
690
|
+
pane_widths=['25em',1,'2em'], pane_heights=[0,1,0], # footer only has uselessoutputs
|
|
691
|
+
height=height
|
|
703
692
|
)
|
|
704
|
-
|
|
705
|
-
self._tsd.observe(self._change_theme, "value")
|
|
706
|
-
self._click.observe(self._click_save_data, "value")
|
|
707
|
-
self._ktcicks.observe(self._warn_update, "value")
|
|
708
|
-
self._krange.observe(self._set_krange, "value")
|
|
709
|
-
self._brange.observe(self._warn_update, "value")
|
|
710
693
|
|
|
711
|
-
@
|
|
712
|
-
def
|
|
713
|
-
|
|
714
|
-
return
|
|
694
|
+
@traitlets.validate('selected_data','clicked_data')
|
|
695
|
+
def _flatten_dict(self, proposal):
|
|
696
|
+
data = proposal['value']
|
|
697
|
+
if data is None: return None # allow None stuff
|
|
715
698
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if not hasattr(self, '_interact'): return # First time not availablebu
|
|
723
|
-
self._interact.output_widget.clear_output(wait=True) # Why need again?
|
|
724
|
-
with self._interact.output_widget:
|
|
725
|
-
self._bands = (
|
|
726
|
-
vp.Vasprun(path) if path.parts[-1].endswith('xml') else vp.Vaspout(path)
|
|
727
|
-
).bands
|
|
728
|
-
self._ppicks.update(self.bands.source.summary)
|
|
729
|
-
|
|
730
|
-
self._krange.max = self.bands.source.summary.NKPTS - 1
|
|
731
|
-
self._krange.tooltip = f"Includes {self.bands.source.get_skipk()} non-zero weight kpoints"
|
|
732
|
-
self.bands.source.set_skipk(0) # full range to view for slider flexibility after fix above
|
|
733
|
-
|
|
734
|
-
self._kwargs['kpairs'] = [self._krange.value,]
|
|
735
|
-
if (ticks := ", ".join(
|
|
736
|
-
f"{k}:{v}" for k, v in self.bands.get_kticks()
|
|
737
|
-
)): # Do not overwrite if empty
|
|
738
|
-
self._ktcicks.value = ticks
|
|
739
|
-
|
|
740
|
-
self._brange.max = self.bands.source.summary.NBANDS
|
|
741
|
-
if self.bands.source.summary.LSORBIT:
|
|
742
|
-
self._click.options = ["None", "vbm", "cbm", "so_max", "so_min"]
|
|
743
|
-
else:
|
|
744
|
-
self._click.options = ["None", "vbm", "cbm"]
|
|
745
|
-
|
|
746
|
-
if (file := path.parent / "result.json").is_file():
|
|
747
|
-
self._result = serializer.load(str(file.absolute())) # Old data loaded
|
|
748
|
-
|
|
749
|
-
pdata = self.bands.source.poscar.data
|
|
750
|
-
self._result.update(
|
|
751
|
-
{
|
|
752
|
-
"v": round(pdata.volume, 4),
|
|
753
|
-
**{k: round(v, 4) for k, v in zip("abc", pdata.norms)},
|
|
754
|
-
**{k: round(v, 4) for k, v in zip(["alpha","beta","gamma"], pdata.angles)},
|
|
755
|
-
}
|
|
756
|
-
)
|
|
757
|
-
self._click_save_data(None) # Load into view
|
|
758
|
-
self._warn_update(None)
|
|
759
|
-
|
|
760
|
-
@property
|
|
761
|
-
def source(self):
|
|
762
|
-
"Returns data source object such as Vasprun or Vaspout."
|
|
763
|
-
return self.bands.source
|
|
764
|
-
|
|
765
|
-
@property
|
|
766
|
-
def bands(self):
|
|
767
|
-
"Bands class initialized"
|
|
768
|
-
if not self._bands:
|
|
769
|
-
raise ValueError("No data loaded by BandsWidget yet!")
|
|
770
|
-
return self._bands
|
|
771
|
-
|
|
772
|
-
@property
|
|
773
|
-
def kwargs(self):
|
|
774
|
-
"Selected kwargs from GUI"
|
|
775
|
-
return self._kwargs
|
|
699
|
+
if not isinstance(data, dict):
|
|
700
|
+
raise traitlets.TraitError(f"Expected a dict for selected_data, got {type(data)}")
|
|
701
|
+
|
|
702
|
+
_data = {k:v for k,v in data.items() if k != 'customdata' and 'indexes' not in k}
|
|
703
|
+
_data.update(pd.DataFrame(data.get('customdata',{})).to_dict(orient='list'))
|
|
704
|
+
return _data # since we know customdata, we can flatten dict
|
|
776
705
|
|
|
777
|
-
@property
|
|
778
|
-
def clicked_data(self):
|
|
779
|
-
"Clicked data from graph"
|
|
780
|
-
return self._click_dict.get("data", None)
|
|
781
706
|
|
|
782
|
-
@
|
|
783
|
-
def
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
callback=lambda trace: self._click_save_data("CLICK"),
|
|
822
|
-
) # 'CLICK' is needed to inntercept in a function
|
|
823
|
-
store_selected_data(self._fig, self._select_dict, callback=None)
|
|
824
|
-
self._ppicks.button.description = "Update Graph"
|
|
825
|
-
|
|
826
|
-
def _change_theme(self, change):
|
|
827
|
-
self._fig.layout.template = self._tsd.value
|
|
828
|
-
|
|
829
|
-
def _set_krange(self, change):
|
|
830
|
-
self._kwargs["kpairs"] = [self._krange.value,]
|
|
831
|
-
self._warn_update(None) # Update warning
|
|
832
|
-
|
|
833
|
-
def _click_save_data(self, change=None):
|
|
834
|
-
def _show_and_save(data_dict):
|
|
835
|
-
self._interact.output_widget.clear_output(wait=True) # Why need again?
|
|
836
|
-
with self._interact.output_widget:
|
|
837
|
-
print(pformat({key: value
|
|
838
|
-
for key, value in data_dict.items()
|
|
839
|
-
if key not in ("so_max", "so_min")
|
|
840
|
-
}))
|
|
841
|
-
|
|
842
|
-
serializer.dump(
|
|
843
|
-
data_dict,
|
|
844
|
-
format="json",
|
|
845
|
-
outfile=self.path.parent / "result.json",
|
|
846
|
-
)
|
|
707
|
+
@ei.callback
|
|
708
|
+
def _update_theme(self, fig, theme):
|
|
709
|
+
super()._update_theme(fig, theme)
|
|
710
|
+
self._kb_fig.layout.template = fig.layout.template
|
|
711
|
+
self._kb_fig.layout.autosize = True
|
|
712
|
+
|
|
713
|
+
def _interactive_params(self):
|
|
714
|
+
return dict(
|
|
715
|
+
fig = self._fig, theme = self._theme, # include theme and fig
|
|
716
|
+
kb_fig = self._kb_fig, # show selected data
|
|
717
|
+
head = ipw.HTML("<b>Band Structure Visualizer</b>"),
|
|
718
|
+
file = self.files.to_dropdown(),
|
|
719
|
+
ppicks = PropsPicker(),
|
|
720
|
+
button = Button(description="Update Graph", icon= 'update'),
|
|
721
|
+
krange = ipw.IntRangeSlider(description="kpoints",min=0, max=1,value=[0,1], tooltip="Includes non-zero weight kpoints"),
|
|
722
|
+
kticks = Text(description="kticks", tooltip="0 index maps to minimum value of kpoints slider."),
|
|
723
|
+
brange = ipw.IntRangeSlider(description="bands",min=1, max=1), # number, not index
|
|
724
|
+
cpoint = ipw.ToggleButtons(description="Select from options and click on figure to store data points",
|
|
725
|
+
value=None, options=["vbm", "cbm", *self._extra_clicks]).add_class('content-width-button'), # the point where clicked
|
|
726
|
+
showft = ipw.IntSlider(description = 'h', orientation='vertical',min=0,max=50, value=0,tooltip="outputs area's height ratio"),
|
|
727
|
+
cdata = 'fig.clicked',
|
|
728
|
+
projs = 'ppicks.projections', # for visual feedback on button
|
|
729
|
+
sdata = '.selected_data',
|
|
730
|
+
hdata = ipw.HTML(), # to show data in one place
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
@ei.callback('out-selected')
|
|
734
|
+
def _plot_data(self, kb_fig, sdata):
|
|
735
|
+
kb_fig.data = [] # clear in any case to avoid confusion
|
|
736
|
+
if not sdata: return # no change
|
|
737
|
+
|
|
738
|
+
df = pd.DataFrame(sdata)
|
|
739
|
+
if 'r' in sdata:
|
|
740
|
+
arr = df[['r','g','b']].to_numpy()
|
|
741
|
+
arr[arr == ''] = 0
|
|
742
|
+
arr, fmt = arr / (arr.max() or 1), lambda v : int(v*255) # color norms
|
|
743
|
+
df['color'] = [f"rgb({fmt(r)},{fmt(g)},{fmt(b)})" for r,g,b in arr]
|
|
744
|
+
else:
|
|
745
|
+
df['color'] = sdata['occ']
|
|
847
746
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if self._click.value and self._click.value == "None":
|
|
852
|
-
return # No need to act on None
|
|
747
|
+
df['msize'] = df['occ']*7 + 10
|
|
748
|
+
cdata = (df[["ys","occ","r","g","b"]] if 'r' in sdata else df[['ys','occ']]).to_numpy()
|
|
749
|
+
rgb_temp = '<br>orbs: (%{customdata[2]},%{customdata[3]},%{customdata[4]})' if 'r' in sdata else ''
|
|
853
750
|
|
|
751
|
+
kb_fig.add_trace(go.Scatter(x=df.nk, y = df.nb, mode = 'markers', marker = dict(size=df.msize,color=df.color), customdata=cdata))
|
|
752
|
+
kb_fig.update_traces(hovertemplate=f"nk: %{{x}}, nb: %{{y}})<br>en: %{{customdata[0]:.4f}}<br>occ: %{{customdata[1]:.4f}}{rgb_temp}<extra></extra>")
|
|
753
|
+
kb_fig.update_layout(template = self._fig.layout.template, autosize=True,
|
|
754
|
+
title = "Selected Data", showlegend=False,coloraxis_showscale=False,
|
|
755
|
+
margin=dict(l=40, r=0, b=40, t=40, pad=0),font=dict(family="stix, serif", size=14)
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
@ei.callback('out-data')
|
|
759
|
+
def _load_data(self, file):
|
|
760
|
+
if not file: return # First time not available
|
|
761
|
+
self._bands = (
|
|
762
|
+
vp.Vasprun(file) if file.parts[-1].endswith('xml') else vp.Vaspout(file)
|
|
763
|
+
).bands
|
|
764
|
+
self.params.ppicks.update(self.bands.source.summary)
|
|
765
|
+
self.params.krange.max = self.bands.source.summary.NKPTS - 1
|
|
766
|
+
self.params.krange.tooltip = f"Includes {self.bands.source.get_skipk()} non-zero weight kpoints"
|
|
767
|
+
self.bands.source.set_skipk(0) # full range to view for slider flexibility after fix above
|
|
768
|
+
self._kws['kpairs'] = [self.params.krange.value,]
|
|
769
|
+
if (ticks := ", ".join(
|
|
770
|
+
f"{k}:{v}" for k, v in self.bands.get_kticks()
|
|
771
|
+
)): # Do not overwrite if empty
|
|
772
|
+
self.params.kticks.value = ticks
|
|
773
|
+
|
|
774
|
+
self.params.brange.max = self.bands.source.summary.NBANDS
|
|
775
|
+
if self.bands.source.summary.LSORBIT:
|
|
776
|
+
self.params.cpoint.options = ["vbm", "cbm", "so_max", "so_min", *self._extra_clicks]
|
|
777
|
+
else:
|
|
778
|
+
self.params.cpoint.options = ["vbm", "cbm",*self._extra_clicks]
|
|
779
|
+
if (path := file.parent / "result.json").is_file():
|
|
780
|
+
self._result = _clean_legacy_data(path)
|
|
781
|
+
|
|
782
|
+
pdata = self.bands.source.poscar.data
|
|
783
|
+
self._result.update(
|
|
784
|
+
{
|
|
785
|
+
"sys": pdata.SYSTEM, "v": round(pdata.volume, 4),
|
|
786
|
+
**{k: round(v, 4) for k, v in zip("abc", pdata.norms)},
|
|
787
|
+
**{k: round(v, 4) for k, v in zip(["alpha","beta","gamma"], pdata.angles)},
|
|
788
|
+
}
|
|
789
|
+
)
|
|
790
|
+
self._show_data(self._result) # Load into view
|
|
791
|
+
|
|
792
|
+
@ei.callback
|
|
793
|
+
def _toggle_footer(self, showft):
|
|
794
|
+
self._app.pane_heights = [0,100 - showft, showft]
|
|
795
|
+
|
|
796
|
+
@ei.callback
|
|
797
|
+
def _set_krange(self, krange):
|
|
798
|
+
self._kws["kpairs"] = [krange,]
|
|
799
|
+
|
|
800
|
+
@ei.callback
|
|
801
|
+
def _warn_update(self, file, kticks, brange, krange, projs):
|
|
802
|
+
self.params.button.description = "🔴 Update Graph"
|
|
803
|
+
|
|
804
|
+
@ei.callback('out-graph')
|
|
805
|
+
def _update_graph(self, fig, button):
|
|
806
|
+
if not self.bands: return # First time not available
|
|
807
|
+
fig.layout.autosize = True # must
|
|
808
|
+
hsk = [
|
|
809
|
+
[v.strip() for v in vs.split(":")]
|
|
810
|
+
for vs in self.params.kticks.value.split(",")
|
|
811
|
+
]
|
|
812
|
+
kmin, kmax = self.params.krange.value or [0,0]
|
|
813
|
+
kticks = [(int(vs[0]), vs[1])
|
|
814
|
+
for vs in hsk # We are going to pick kticks silently in given range
|
|
815
|
+
if len(vs) == 2 and abs(int(vs[0])) < (kmax - kmin) # handle negative indices too
|
|
816
|
+
] or None
|
|
817
|
+
|
|
818
|
+
_bands = None
|
|
819
|
+
if self.params.brange.value:
|
|
820
|
+
l, h = self.params.brange.value
|
|
821
|
+
_bands = range(l-1, h) # from number to index
|
|
822
|
+
|
|
823
|
+
self._kws = {**self._kws, "kticks": kticks, "bands": _bands}
|
|
824
|
+
ISPIN = self.bands.source.summary.ISPIN
|
|
825
|
+
if self.params.ppicks.projections:
|
|
826
|
+
self._kws["projections"] = self.params.ppicks.projections
|
|
827
|
+
_fig = self.bands.iplot_rgb_lines(**self._kws, name="Up" if ISPIN == 2 else "")
|
|
828
|
+
if ISPIN == 2:
|
|
829
|
+
self.bands.iplot_rgb_lines(**self._kws, spin=1, name="Down", fig=fig)
|
|
830
|
+
|
|
831
|
+
self.iplot = partial(self.bands.iplot_rgb_lines, **self._kws)
|
|
832
|
+
self.splot = partial(self.bands.splot_rgb_lines, **self._kws)
|
|
833
|
+
else:
|
|
834
|
+
self._kws.pop("projections",None) # may be previous one
|
|
835
|
+
_fig = self.bands.iplot_bands(**self._kws, name="Up" if ISPIN == 2 else "")
|
|
836
|
+
if self.bands.source.summary.ISPIN == 2:
|
|
837
|
+
self.bands.iplot_bands(**self._kws, spin=1, name="Down", fig=fig)
|
|
838
|
+
|
|
839
|
+
self.iplot = partial(self.bands.iplot_bands, **self._kws)
|
|
840
|
+
self.splot = partial(self.bands.splot_bands, **self._kws)
|
|
841
|
+
|
|
842
|
+
ptk.iplot2widget(_fig, fig, template=fig.layout.template)
|
|
843
|
+
fig.clicked = {} # avoid data from previous figure
|
|
844
|
+
fig.selected = {} # avoid data from previous figure
|
|
845
|
+
button.description = "Update Graph" # clear trigger
|
|
846
|
+
|
|
847
|
+
@ei.callback('out-click')
|
|
848
|
+
def _click_save_data(self, cdata):
|
|
849
|
+
if self.params.cpoint.value is None: return # at reset-
|
|
854
850
|
data_dict = self._result.copy() # Copy old data
|
|
855
851
|
|
|
856
|
-
if
|
|
857
|
-
|
|
858
|
-
|
|
852
|
+
if cdata: # No need to make empty dict
|
|
853
|
+
key = self.params.cpoint.value
|
|
854
|
+
if key:
|
|
855
|
+
y = round(float(*cdata['ys']) + self.bands.data.ezero, 6) # Add ezero
|
|
856
|
+
if not key in self._extra_clicks:
|
|
857
|
+
data_dict[key] = y # Assign value back
|
|
858
|
+
|
|
859
|
+
if not key.startswith("so_"): # not spin-orbit points
|
|
860
|
+
cst, = cdata.get('customdata',[{}]) # single item
|
|
861
|
+
kp = [cst.get(f"k{n}", None) for n in 'xyz']
|
|
862
|
+
kp = tuple([round(k,6) if k else k for k in kp])
|
|
863
|
+
|
|
864
|
+
if key in ("vbm","cbm"):
|
|
865
|
+
data_dict[f"k{key}"] = kp
|
|
866
|
+
else: # user points, stor both for reference
|
|
867
|
+
data_dict[key] = {"k":kp,"e":y}
|
|
859
868
|
|
|
860
|
-
if key := self._click.value:
|
|
861
|
-
data_dict[key] = y # Assign value back
|
|
862
|
-
if not key.startswith("so"):
|
|
863
|
-
data_dict[key + "_k"] = round(
|
|
864
|
-
x, 6
|
|
865
|
-
) # Save x to test direct/indirect
|
|
866
869
|
|
|
867
870
|
if data_dict.get("vbm", None) and data_dict.get("cbm", None):
|
|
868
871
|
data_dict["gap"] = np.round(data_dict["cbm"] - data_dict["vbm"], 6)
|
|
@@ -873,22 +876,55 @@ class BandsWidget(VBox):
|
|
|
873
876
|
)
|
|
874
877
|
|
|
875
878
|
self._result.update(data_dict) # store new data
|
|
876
|
-
_show_and_save(self._result)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
879
|
+
self._show_and_save(self._result, f"{key} = {data_dict[key]}")
|
|
880
|
+
self.params.cpoint.value = None # Reset to None to avoid accidental click at end
|
|
881
|
+
|
|
882
|
+
def _show_data(self, data, last_click=None):
|
|
883
|
+
"Show data in html widget, no matter where it was called."
|
|
884
|
+
keys = "sys vbm cbm gap direct soc v a b c alpha beta gamma".split()
|
|
885
|
+
data = {key:data[key] for key in keys if key in data} # show only standard data
|
|
886
|
+
kv, kc = [self._result.get(k,[None]*3) for k in ('kvbm','kcbm')]
|
|
887
|
+
data['direct'] = (kv == kc) if None not in kv else False
|
|
888
|
+
|
|
889
|
+
# Add a caption to the table
|
|
890
|
+
caption = f"<caption style='caption-side:bottom; opacity:0.7;'><code>{last_click or 'clicked data is shown here'}</code></caption>"
|
|
891
|
+
|
|
892
|
+
headers = "".join(f"<th>{key}</th>" for key in data.keys())
|
|
893
|
+
values = "".join(f"<td>{format(value, '.4f') if isinstance(value, float) else value}</td>" for value in data.values())
|
|
894
|
+
self.params.hdata.value = f"""<table border='1' style='width:100%;max-width:100% !important;border-collapse:collapse;'>
|
|
895
|
+
{caption}<tr>{headers}</tr>\n<tr>{values}</tr></table>"""
|
|
896
|
+
|
|
897
|
+
def _show_and_save(self, data_dict, last_click=None):
|
|
898
|
+
self._show_data(data_dict,last_click=last_click)
|
|
899
|
+
if self.file:
|
|
900
|
+
serializer.dump(data_dict,format="json",
|
|
901
|
+
outfile=self.file.parent / "result.json")
|
|
902
|
+
|
|
903
|
+
def results(self, exclude_keys=None):
|
|
904
|
+
"Generate a dataframe form result.json file in each folder, with optionally excluding keys."
|
|
905
|
+
return load_results(self.params.file.options, exclude_keys=exclude_keys)
|
|
906
|
+
|
|
907
|
+
@property
|
|
908
|
+
def source(self):
|
|
909
|
+
"Returns data source object such as Vasprun or Vaspout."
|
|
910
|
+
return self.bands.source
|
|
880
911
|
|
|
881
|
-
|
|
882
|
-
|
|
912
|
+
@property
|
|
913
|
+
def bands(self):
|
|
914
|
+
"Bands class initialized"
|
|
915
|
+
if not self._bands:
|
|
916
|
+
raise ValueError("No data loaded by BandsWidget yet!")
|
|
917
|
+
return self._bands
|
|
883
918
|
|
|
884
919
|
@property
|
|
885
|
-
def
|
|
886
|
-
"
|
|
887
|
-
return
|
|
920
|
+
def kws(self):
|
|
921
|
+
"Selected keyword arguments from GUI"
|
|
922
|
+
return self._kws
|
|
923
|
+
|
|
888
924
|
|
|
889
925
|
|
|
890
926
|
@fix_signature
|
|
891
|
-
class
|
|
927
|
+
class KPathWidget(_ThemedFigureInteract):
|
|
892
928
|
"""
|
|
893
929
|
Interactively bulid a kpath for bandstructure calculation.
|
|
894
930
|
|
|
@@ -900,159 +936,164 @@ class KpathWidget(VBox):
|
|
|
900
936
|
- Add labels to the points by typing in the "Labels" box such as "Γ,X" or "Γ 5,X" that will add 5 points in interval.
|
|
901
937
|
- To break the path between two points "Γ" and "X" type "Γ 0,X" in the "Labels" box, zero means no points in interval.
|
|
902
938
|
|
|
903
|
-
You can use `self.files.update` method to change source files without effecting state of widget.
|
|
939
|
+
- You can use `self.files.update` method to change source files without effecting state of widget.
|
|
940
|
+
- You can observe `self.file` trait to get current file selected and plot something, e.g. lattice structure.
|
|
904
941
|
"""
|
|
942
|
+
file = traitlets.Any(None, allow_none=True)
|
|
905
943
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
self.
|
|
911
|
-
self._kpt = Text(description="KPOINT", continuous_update=False)
|
|
912
|
-
self._add = Button(description="Lock", tooltip="Lock/Unlock adding more points")
|
|
913
|
-
self._del = Button(description="❌ Point", tooltip="Delete Selected Points")
|
|
914
|
-
self._tsb = Button(description="Dark Plot", tooltip="Toggle Plot Theme")
|
|
944
|
+
@property
|
|
945
|
+
def poscar(self): return self._poscar
|
|
946
|
+
|
|
947
|
+
def __init__(self, files, height="450px"):
|
|
948
|
+
self.add_class("KPathWidget")
|
|
915
949
|
self._poscar = None
|
|
916
|
-
self.
|
|
950
|
+
self._oldclick = None
|
|
917
951
|
self._kpoints = {}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
self.
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
Files(files)._attributed_interactive(self,
|
|
930
|
-
self._update_fig, self._fig, free_widgets=free_widgets, height=height
|
|
952
|
+
self._files = Files(files) # set name _files to ensure access to files
|
|
953
|
+
super().__init__()
|
|
954
|
+
traitlets.dlink((self.params.file,'value'),(self, 'file')) # update file trait
|
|
955
|
+
|
|
956
|
+
btns = [HBox(layout=Layout(min_height="24px")),('lock','delp', 'theme')]
|
|
957
|
+
self.relayout(
|
|
958
|
+
left_sidebar=['head','file',btns, 'info', 'sm','out-kpt','kpt', 'out-lab', 'lab'],
|
|
959
|
+
center=['fig'], footer = [c for c in self.groups.outputs if not c in ('out-lab','out-kpt')],
|
|
960
|
+
pane_widths=['25em',1,0], pane_heights=[0,1,0], # footer only has uselessoutputs
|
|
961
|
+
height=height
|
|
931
962
|
)
|
|
932
|
-
|
|
933
|
-
self._tsb.on_click(self._update_theme)
|
|
934
|
-
self._add.on_click(self._toggle_lock)
|
|
935
|
-
self._del.on_click(self._del_point)
|
|
936
|
-
self._kpt.observe(self._take_kpt, "value")
|
|
937
|
-
self._lab.observe(self._add_label)
|
|
938
963
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
964
|
+
def _show_info(self, text, color='skyblue'):
|
|
965
|
+
self.params.info.value = f'<span style="color:{color}">{text}</span>'
|
|
966
|
+
|
|
967
|
+
def _interactive_params(self):
|
|
968
|
+
return dict(
|
|
969
|
+
fig = self._fig, theme = self._theme, # include theme and fig
|
|
970
|
+
head = ipw.HTML("<b>K-Path Builder</b>"),
|
|
971
|
+
file = self.files.to_dropdown(), # auto updatable on files.update
|
|
972
|
+
sm = SelectMultiple(description="KPOINTS", options=[], layout=Layout(width="auto")),
|
|
973
|
+
lab = Text(description="Labels", continuous_update=True),
|
|
974
|
+
kpt = Text(description="KPOINT", continuous_update=False),
|
|
975
|
+
delp = Button(description=" ", icon='trash', tooltip="Delete Selected Points"),
|
|
976
|
+
click = 'fig.clicked',
|
|
977
|
+
lock = Button(description=" ", icon='unlock', tooltip="Lock/Unlock adding more points"),
|
|
978
|
+
info = ipw.HTML(), # consise information in one place
|
|
979
|
+
)
|
|
953
980
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
981
|
+
@ei.callback('out-fig')
|
|
982
|
+
def _update_fig(self, file, fig):
|
|
983
|
+
if not file: return # empty one
|
|
957
984
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
kp = [float(k) for k in kp.split("[")[1].split("]")[0].split()]
|
|
993
|
-
|
|
994
|
-
if self._sm.value:
|
|
995
|
-
self._take_kpt(kp) # this updates plot back as well
|
|
996
|
-
elif self._add.description == "Lock": # only add when open
|
|
985
|
+
from ipyvasp.lattice import POSCAR # to avoid circular import
|
|
986
|
+
self._poscar = POSCAR(file)
|
|
987
|
+
ptk.iplot2widget(
|
|
988
|
+
self._poscar.iplot_bz(fill=False, color="red"), fig, self.params.fig.layout.template
|
|
989
|
+
)
|
|
990
|
+
fig.layout.autosize = True # must
|
|
991
|
+
|
|
992
|
+
with fig.batch_animate():
|
|
993
|
+
fig.add_trace(
|
|
994
|
+
go.Scatter3d(x=[], y=[], z=[],
|
|
995
|
+
mode="lines+text",
|
|
996
|
+
name="path",
|
|
997
|
+
text=[],
|
|
998
|
+
hoverinfo="none", # dont let it block other points
|
|
999
|
+
textfont_size=18,
|
|
1000
|
+
)
|
|
1001
|
+
) # add path that will be updated later
|
|
1002
|
+
self._show_info("Click points on plot to store for kpath.")
|
|
1003
|
+
|
|
1004
|
+
@ei.callback('out-click')
|
|
1005
|
+
def _click(self, click):
|
|
1006
|
+
# We are setting value on select multiple to get it done in one click conveniently
|
|
1007
|
+
# But that triggers infinite loop, so we need to check if click is different next time
|
|
1008
|
+
if click != self._oldclick and (tidx := click.get('trace_indexes',[])):
|
|
1009
|
+
self._oldclick = click # for next time
|
|
1010
|
+
data = self.params.fig.data # click depends on fig, so accessing here
|
|
1011
|
+
if not [data[i] for i in tidx if 'HSK' in data[i].name]: return
|
|
1012
|
+
|
|
1013
|
+
if cp := [*click.get('xs', []),*click.get('ys', []),*click.get('zs', [])]:
|
|
1014
|
+
kp = self._poscar.bz.to_fractional(cp) # reciprocal space
|
|
1015
|
+
|
|
1016
|
+
if self.params.sm.value:
|
|
1017
|
+
self._set_kpt(kp) # this updates plot back as well
|
|
1018
|
+
elif self.params.lock.icon == "unlock": # only add when open
|
|
997
1019
|
self._add_point(kp)
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
"
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1020
|
+
|
|
1021
|
+
@ei.callback('out-kpt')
|
|
1022
|
+
def _take_kpt(self, kpt):
|
|
1023
|
+
print("Add kpoint e.g. 0,1,3 at selection(s)")
|
|
1024
|
+
self._set_kpt(kpt)
|
|
1025
|
+
|
|
1026
|
+
@ei.callback('out-lab')
|
|
1027
|
+
def _set_lab(self, lab):
|
|
1028
|
+
print("Add label[:number] e.g. X:5,Y,L:9")
|
|
1029
|
+
self._add_label(lab)
|
|
1030
|
+
|
|
1031
|
+
@ei.callback
|
|
1032
|
+
def _update_theme(self, fig, theme):
|
|
1033
|
+
super()._update_theme(fig, theme) # call parent method, but important
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
@ei.callback
|
|
1037
|
+
def _toggle_lock(self, lock):
|
|
1038
|
+
self.params.lock.icon = 'lock' if self.params.lock.icon == 'unlock' else 'unlock'
|
|
1039
|
+
self._show_info(f"{self.params.lock.icon}ed adding/deleting kpoints!")
|
|
1040
|
+
|
|
1041
|
+
@ei.callback
|
|
1042
|
+
def _del_point(self, delp):
|
|
1043
|
+
if self.params.lock.icon == 'unlock': # Do not delete locked
|
|
1044
|
+
sm = self.params.sm
|
|
1045
|
+
for v in sm.value: # for loop here is important to update selection properly
|
|
1046
|
+
sm.options = [opt for opt in sm.options if opt[1] != v]
|
|
1047
|
+
self._update_selection() # update plot as well
|
|
1048
|
+
else:
|
|
1049
|
+
self._show_info("Select point(s) to delete")
|
|
1050
|
+
else:
|
|
1051
|
+
self._show_info("cannot delete point when locked!", 'red')
|
|
1052
|
+
|
|
1053
|
+
def _add_point(self, kpt):
|
|
1054
|
+
sm = self.params.sm
|
|
1055
|
+
sm.options = [*sm.options, ("⋮", len(sm.options))]
|
|
1056
|
+
# select to receive point as well, this somehow makes infinit loop issues,
|
|
1057
|
+
# but need to work, so self._oldclick is used to check in _click callback
|
|
1058
|
+
sm.value = (sm.options[-1][1],)
|
|
1059
|
+
self._set_kpt(kpt) # add point, label and plot back
|
|
1060
|
+
|
|
1061
|
+
def _set_kpt(self,kpt):
|
|
1062
|
+
point = kpt
|
|
1063
|
+
if isinstance(kpt, str) and kpt:
|
|
1064
|
+
if len(kpt.split(",")) != 3: return # Enter at incomplete input
|
|
1065
|
+
point = [float(v) for v in kpt.split(",")] # kpt is value widget
|
|
1066
|
+
|
|
1067
|
+
if not isinstance(point,(list, tuple,np.ndarray)): return # None etc
|
|
1068
|
+
|
|
1069
|
+
if len(point) != 3:
|
|
1070
|
+
raise ValueError("Expects KPOINT of 3 floats")
|
|
1071
|
+
self._kpoints.update({v: point for v in self.params.sm.value})
|
|
1072
|
+
label = "{:>8.4f} {:>8.4f} {:>8.4f}".format(*point)
|
|
1073
|
+
self.params.sm.options = [
|
|
1074
|
+
(label, value) if value in self.params.sm.value else (lab, value)
|
|
1075
|
+
for (lab, value) in self.params.sm.options
|
|
1076
|
+
]
|
|
1077
|
+
self._add_label(self.params.lab.value) # Re-adjust labels and update plot as well
|
|
1078
|
+
|
|
1079
|
+
def _add_label(self, lab):
|
|
1080
|
+
labs = [" ⋮ " for _ in self.params.sm.options] # as much as options
|
|
1081
|
+
for idx, (_, lb) in enumerate(zip(self.params.sm.options, (lab or "").split(","))):
|
|
1082
|
+
labs[idx] = labs[idx] + lb # don't leave empty anyhow
|
|
1083
|
+
|
|
1084
|
+
self.params.sm.options = [
|
|
1085
|
+
(v.split("⋮")[0].strip() + lb, idx)
|
|
1086
|
+
for (v, idx), lb in zip(self.params.sm.options, labs)
|
|
1087
|
+
]
|
|
1088
|
+
self._update_selection() # Update plot in both cases, by click or manual input
|
|
1046
1089
|
|
|
1047
1090
|
def get_kpoints(self):
|
|
1048
1091
|
"Returns kpoints list including labels and numbers in intervals if given."
|
|
1049
|
-
keys = [
|
|
1050
|
-
idx for (_, idx) in self._sm.options if idx in self._kpoints
|
|
1051
|
-
] # order and existence is important
|
|
1092
|
+
keys = [idx for (_, idx) in self.params.sm.options if idx in self._kpoints] # order and existence is important
|
|
1052
1093
|
kpts = [self._kpoints[k] for k in keys]
|
|
1053
1094
|
LN = [
|
|
1054
1095
|
lab.split("⋮")[1].strip().split()
|
|
1055
|
-
for (lab, idx) in self.
|
|
1096
|
+
for (lab, idx) in self.params.sm.options
|
|
1056
1097
|
if idx in keys
|
|
1057
1098
|
]
|
|
1058
1099
|
|
|
@@ -1072,76 +1113,43 @@ class KpathWidget(VBox):
|
|
|
1072
1113
|
)
|
|
1073
1114
|
return kpts
|
|
1074
1115
|
|
|
1075
|
-
def
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
else
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if self._add.description == "Lock":
|
|
1093
|
-
self._add.description = "Unlock"
|
|
1094
|
-
else:
|
|
1095
|
-
self._add.description = "Lock"
|
|
1096
|
-
|
|
1097
|
-
def _del_point(self, btn):
|
|
1098
|
-
with self._interact.output_widget:
|
|
1099
|
-
for (
|
|
1100
|
-
v
|
|
1101
|
-
) in (
|
|
1102
|
-
self._sm.value
|
|
1103
|
-
): # for loop here is important to update selection properly
|
|
1104
|
-
self._sm.options = [opt for opt in self._sm.options if opt[1] != v]
|
|
1105
|
-
self._update_selection() # update plot as well
|
|
1116
|
+
def get_coords_labels(self):
|
|
1117
|
+
"Returns tuple of (coordinates, labels) to directly plot."
|
|
1118
|
+
points = self.get_kpoints()
|
|
1119
|
+
coords = self.poscar.bz.to_cartesian([p[:3] for p in points]).tolist() if points else []
|
|
1120
|
+
labels = [p[3] if (len(p) >= 4 and isinstance(p[3], str)) else "" for p in points]
|
|
1121
|
+
numbers = [
|
|
1122
|
+
p[4] if len(p) == 5
|
|
1123
|
+
else p[3] if (len(p) == 4 and isinstance(p[3], int))
|
|
1124
|
+
else "" for p in points]
|
|
1125
|
+
|
|
1126
|
+
j = 0
|
|
1127
|
+
for i, n in enumerate(numbers, start=1):
|
|
1128
|
+
if isinstance(n, int) and n == 0:
|
|
1129
|
+
labels.insert(i + j, "")
|
|
1130
|
+
coords.insert(i + j, [np.nan, np.nan, np.nan])
|
|
1131
|
+
j += 1
|
|
1132
|
+
return np.array(coords), labels
|
|
1106
1133
|
|
|
1107
|
-
def
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
self._kpoints.update({v: point for v in self._sm.value})
|
|
1118
|
-
label = "{:>8.4f} {:>8.4f} {:>8.4f}".format(*point)
|
|
1119
|
-
self._sm.options = [
|
|
1120
|
-
(label, value) if value in self._sm.value else (lab, value)
|
|
1121
|
-
for (lab, value) in self._sm.options
|
|
1122
|
-
]
|
|
1123
|
-
self._add_label(None) # Re-adjust labels and update plot as well
|
|
1124
|
-
|
|
1125
|
-
def _add_label(self, change):
|
|
1126
|
-
with self._interact.output_widget:
|
|
1127
|
-
labs = [" ⋮ " for _ in self._sm.options] # as much as options
|
|
1128
|
-
for idx, (_, lab) in enumerate(
|
|
1129
|
-
zip(self._sm.options, self._lab.value.split(","))
|
|
1130
|
-
):
|
|
1131
|
-
labs[idx] = labs[idx] + lab # don't leave empty anyhow
|
|
1132
|
-
|
|
1133
|
-
self._sm.options = [
|
|
1134
|
-
(v.split("⋮")[0].strip() + lab, idx)
|
|
1135
|
-
for (v, idx), lab in zip(self._sm.options, labs)
|
|
1136
|
-
]
|
|
1137
|
-
|
|
1138
|
-
self._update_selection() # Update plot in both cases, by click or manual input
|
|
1134
|
+
def _update_selection(self):
|
|
1135
|
+
coords, labels = self.get_coords_labels()
|
|
1136
|
+
with self.params.fig.batch_animate():
|
|
1137
|
+
for trace in self.params.fig.data:
|
|
1138
|
+
if "path" in trace.name and coords.any():
|
|
1139
|
+
trace.x = coords[:, 0]
|
|
1140
|
+
trace.y = coords[:, 1]
|
|
1141
|
+
trace.z = coords[:, 2]
|
|
1142
|
+
trace.text = _fmt_labels(labels) # convert latex to html equivalent
|
|
1139
1143
|
|
|
1140
1144
|
@_sub_doc(lat.get_kpath, {"kpoints :.*n :": "n :", "rec_basis :.*\n\n": "\n\n"})
|
|
1141
1145
|
@_sig_kwargs(lat.get_kpath, ("kpoints", "rec_basis"))
|
|
1142
1146
|
def get_kpath(self, n=5, **kwargs):
|
|
1143
1147
|
return self.poscar.get_kpath(self.get_kpoints(), n=n, **kwargs)
|
|
1144
1148
|
|
|
1149
|
+
def iplot(self):
|
|
1150
|
+
"Returns disconnected current plotly figure"
|
|
1151
|
+
return go.Figure(data=self.params.fig.data, layout=self.params.fig.layout)
|
|
1152
|
+
|
|
1145
1153
|
def splot(self, plane=None, fmt_label=lambda x: x, plot_kws={}, **kwargs):
|
|
1146
1154
|
"""
|
|
1147
1155
|
Same as `ipyvasp.lattice.POSCAR.splot_bz` except it also plots path on BZ.
|
|
@@ -1165,10 +1173,5 @@ class KpathWidget(VBox):
|
|
|
1165
1173
|
) # plots on ax automatically
|
|
1166
1174
|
return ax
|
|
1167
1175
|
|
|
1168
|
-
def iplot(self):
|
|
1169
|
-
"Returns disconnected current plotly figure"
|
|
1170
|
-
return go.Figure(data=self._fig.data, layout=self._fig.layout)
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
1176
|
# Should be at end
|
|
1174
1177
|
del fix_signature # no more need
|