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