ipyvasp 0.9.91__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 +622 -717
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.93.dist-info}/METADATA +6 -2
- ipyvasp-0.9.93.dist-info/RECORD +25 -0
- ipyvasp-0.9.91.dist-info/RECORD +0 -25
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.93.dist-info}/LICENSE +0 -0
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.93.dist-info}/WHEEL +0 -0
- {ipyvasp-0.9.91.dist-info → ipyvasp-0.9.93.dist-info}/entry_points.txt +0 -0
- {ipyvasp-0.9.91.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
|
|
@@ -202,7 +200,7 @@ class Files:
|
|
|
202
200
|
lines = [(k,v) for k,v in lines if k in tags]
|
|
203
201
|
d = {k:v for k,v in lines if not k.startswith('#')}
|
|
204
202
|
d.update({k:len(v) for k,v in p.types.items()})
|
|
205
|
-
d.update(zip('
|
|
203
|
+
d.update(zip(['a','b','c','v','alpha','beta','gamma'], [*p.norms,p.volume,*p.angles]))
|
|
206
204
|
return d
|
|
207
205
|
|
|
208
206
|
return self.with_name('POSCAR').summarize(info, tags=tags)
|
|
@@ -219,6 +217,18 @@ class Files:
|
|
|
219
217
|
if old in dd.options:
|
|
220
218
|
dd.value = old
|
|
221
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
|
+
|
|
222
232
|
def add(self, path_or_files, glob = '*', exclude=None, **kwargs):
|
|
223
233
|
"""Add more files or with a diffrent glob on top of exitsing files. Returns same instance.
|
|
224
234
|
Useful to add multiple globbed files into a single chained call.
|
|
@@ -231,190 +241,39 @@ class Files:
|
|
|
231
241
|
def _unique(self, *files_tuples):
|
|
232
242
|
return tuple(np.unique(np.hstack(files_tuples)))
|
|
233
243
|
|
|
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')
|
|
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!")
|
|
297
248
|
|
|
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.")
|
|
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
|
|
317
258
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
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!")
|
|
321
261
|
|
|
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
|
|
353
|
-
|
|
354
|
-
def _attributed_interactive(self, box, func, *args, **kwargs):
|
|
355
|
-
box._files = self
|
|
356
|
-
box._interact = self.interactive(func, *args, **kwargs)
|
|
357
|
-
box.children = box._interact.children
|
|
358
|
-
box._files._dd = box._interact._dd
|
|
262
|
+
return ei.interactive(*funcs,auto_update=auto_update, app_layout = app_layout, grid_css=grid_css, file = self.to_dropdown(), **kwargs)
|
|
359
263
|
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
264
|
+
@_sub_doc(ei.interact)
|
|
265
|
+
def interact(self, *funcs, auto_update=True, app_layout=None, grid_css={},**kwargs):
|
|
404
266
|
def inner(func):
|
|
405
|
-
display(self.interactive(func, *
|
|
406
|
-
|
|
407
|
-
options=options,
|
|
408
|
-
height=height,
|
|
409
|
-
panel_size=panel_size,
|
|
267
|
+
display(self.interactive(func, *funcs,
|
|
268
|
+
auto_update=auto_update, app_layout = app_layout, grid_css=grid_css,
|
|
410
269
|
**kwargs)
|
|
411
270
|
)
|
|
412
271
|
return func
|
|
413
272
|
return inner
|
|
414
273
|
|
|
415
274
|
def kpath_widget(self, height='400px'):
|
|
416
|
-
"Get
|
|
417
|
-
return
|
|
275
|
+
"Get KPathWidget instance with these files."
|
|
276
|
+
return KPathWidget(files = self.with_name('POSCAR'), height = height)
|
|
418
277
|
|
|
419
278
|
def bands_widget(self, height='450px'):
|
|
420
279
|
"Get BandsWidget instance with these files."
|
|
@@ -469,86 +328,103 @@ class Files:
|
|
|
469
328
|
return self.summarize(lambda path: {"size": get_file_size(path)})
|
|
470
329
|
|
|
471
330
|
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
self._widgets["atoms"].observe(observe_change, "value")
|
|
486
|
-
self._widgets["orbs"].observe(observe_change, "value")
|
|
487
|
-
|
|
488
|
-
self._atoms = {}
|
|
489
|
-
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')
|
|
490
348
|
self._process(system_summary)
|
|
491
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
|
+
|
|
492
363
|
def _process(self, system_summary):
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
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
|
|
498
368
|
|
|
499
|
-
self.children = [self._widgets["atoms"], self._widgets["orbs"]]
|
|
500
369
|
sorbs = system_summary.orbs
|
|
370
|
+
self._orbs_map = {"-": [], "All": range(len(sorbs)), "s": [0]}
|
|
501
371
|
|
|
502
|
-
|
|
372
|
+
# p-orbitals
|
|
503
373
|
if set(["px", "py", "pz"]).issubset(sorbs):
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
idx for idx, key in enumerate(sorbs) if key in ("px", "py")
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
509
381
|
if set(["dxy", "dyz"]).issubset(sorbs):
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
512
388
|
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
|
-
|
|
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
|
|
535
413
|
|
|
536
414
|
def update(self, system_summary):
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return items
|
|
548
|
-
|
|
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
|
|
549
425
|
|
|
550
426
|
@fix_signature
|
|
551
|
-
class PropsPicker(VBox):
|
|
427
|
+
class PropsPicker(VBox): # NOTE: remove New Later
|
|
552
428
|
"""
|
|
553
429
|
A widget to pick atoms and orbitals for plotting.
|
|
554
430
|
|
|
@@ -556,88 +432,48 @@ class PropsPicker(VBox):
|
|
|
556
432
|
----------
|
|
557
433
|
system_summary : (Vasprun,Vaspout).summary
|
|
558
434
|
N : int, default is 3, number of projections to pick.
|
|
559
|
-
|
|
560
|
-
|
|
435
|
+
|
|
436
|
+
You can observe `projections` trait.
|
|
561
437
|
"""
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
):
|
|
438
|
+
projections = traitlets.Dict({})
|
|
439
|
+
|
|
440
|
+
def __init__(self, system_summary=None, N=3):
|
|
566
441
|
super().__init__()
|
|
567
|
-
self.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
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)],
|
|
576
450
|
)
|
|
577
|
-
self.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
|
|
592
473
|
def update(self, system_summary):
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
@property
|
|
597
|
-
def button(self):
|
|
598
|
-
return self._button
|
|
599
|
-
|
|
600
|
-
@property
|
|
601
|
-
def projections(self):
|
|
602
|
-
out = {}
|
|
603
|
-
for child in self._stacked.children:
|
|
604
|
-
props = child.props
|
|
605
|
-
if props["atoms"] and props["orbs"]: # discard empty
|
|
606
|
-
out[props["label"]] = (props["atoms"], props["orbs"])
|
|
607
|
-
|
|
608
|
-
return out
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def __store_figclick_data(fig, store_dict, callback=None, selection=False):
|
|
612
|
-
"Store clicked data in a dict. callback takes trace as argument and is called after storing data."
|
|
613
|
-
if not isinstance(fig, go.FigureWidget):
|
|
614
|
-
raise TypeError("fig must be a FigureWidget")
|
|
615
|
-
if not isinstance(store_dict, dict):
|
|
616
|
-
raise TypeError("store_dict must be a dict")
|
|
617
|
-
if callback and not callable(callback):
|
|
618
|
-
raise TypeError("callback must be callable if given")
|
|
619
|
-
|
|
620
|
-
def handle_click(trace, points, state):
|
|
621
|
-
store_dict["data"] = points
|
|
622
|
-
if callback:
|
|
623
|
-
callback(trace)
|
|
624
|
-
|
|
625
|
-
for trace in fig.data:
|
|
626
|
-
if selection:
|
|
627
|
-
trace.on_selection(handle_click)
|
|
628
|
-
else:
|
|
629
|
-
trace.on_click(handle_click)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
def store_clicked_data(fig, store_dict, callback=None):
|
|
633
|
-
"Store clicked point data to a store_dict. callback takes trace being clicked as argument."
|
|
634
|
-
return __store_figclick_data(fig, store_dict, callback, selection=False)
|
|
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
|
-
|
|
474
|
+
"""Update all pickers with new system data"""
|
|
475
|
+
for picker in self._pickers:
|
|
476
|
+
picker.update(system_summary)
|
|
641
477
|
|
|
642
478
|
def load_results(paths_list):
|
|
643
479
|
"Loads result.json from paths_list and returns a dataframe."
|
|
@@ -658,211 +494,285 @@ def load_results(paths_list):
|
|
|
658
494
|
|
|
659
495
|
return summarize(result_paths, load_data)
|
|
660
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)
|
|
661
576
|
|
|
662
577
|
@fix_signature
|
|
663
|
-
class BandsWidget(
|
|
578
|
+
class BandsWidget(_ThemedFigureInteract):
|
|
664
579
|
"""Visualize band structure from VASP calculation. You can click on the graph to get the data such as VBM, CBM, etc.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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.
|
|
669
589
|
"""
|
|
590
|
+
file = traitlets.Any(allow_none=True)
|
|
591
|
+
clicked_data = traitlets.Dict(allow_none=True)
|
|
592
|
+
selected_data = traitlets.Dict(allow_none=True)
|
|
670
593
|
|
|
671
594
|
def __init__(self, files, height="450px"):
|
|
672
|
-
|
|
595
|
+
self.add_class("BandsWidget")
|
|
596
|
+
self._files = Files(files)
|
|
673
597
|
self._bands = None
|
|
674
|
-
self.
|
|
675
|
-
self.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
self.
|
|
679
|
-
self.
|
|
680
|
-
self.
|
|
681
|
-
self._krange = ipw.IntRangeSlider(description="kpoints",min=0, max=1,value=[0,1], tooltip="Includes non-zero weight kpoints")
|
|
682
|
-
self._ppicks = PropsPicker(
|
|
683
|
-
on_button_click=self._update_graph, on_selection_changed=self._warn_update
|
|
684
|
-
)
|
|
685
|
-
self._ppicks.button.description = "Update Graph"
|
|
686
|
-
self._result = {} # store and save output results
|
|
687
|
-
self._click_dict = {} # store clicked data
|
|
688
|
-
self._select_dict = {} # store selection data
|
|
689
|
-
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'))
|
|
690
605
|
|
|
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,
|
|
606
|
+
self.relayout(
|
|
607
|
+
left_sidebar=[
|
|
608
|
+
'head','file','krange','kticks','brange', 'ppicks',
|
|
609
|
+
[HBox(),('theme','button')],
|
|
701
610
|
],
|
|
702
|
-
|
|
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
|
|
703
615
|
)
|
|
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
616
|
|
|
711
|
-
@
|
|
712
|
-
def
|
|
713
|
-
|
|
714
|
-
return self._interact._dd.value
|
|
617
|
+
@ei.callback
|
|
618
|
+
def _update_theme(self, fig, theme):
|
|
619
|
+
return super()._update_theme(fig, theme)
|
|
715
620
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
|
720
672
|
|
|
721
|
-
def
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
|
739
700
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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)
|
|
701
|
+
# save cleaned data
|
|
702
|
+
serializer.dump(new_data,format="json",outfile=path)
|
|
703
|
+
return new_data
|
|
759
704
|
|
|
760
|
-
@
|
|
761
|
-
def
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
764
735
|
|
|
765
|
-
|
|
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
|
|
736
|
+
self._kws = {**self._kws, "kticks": kticks, "bands": _bands}
|
|
771
737
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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)
|
|
776
743
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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)
|
|
781
750
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
"Data selected by box or lasso selection from graph"
|
|
785
|
-
return self._select_dict.get("data", None)
|
|
786
|
-
|
|
787
|
-
def _update_graph(self, btn):
|
|
788
|
-
if not hasattr(self, '_interact'): return # First time not available
|
|
789
|
-
self._interact.output_widget.clear_output(wait=True) # Why need again?
|
|
790
|
-
with self._interact.output_widget:
|
|
791
|
-
hsk = [
|
|
792
|
-
[v.strip() for v in vs.split(":")]
|
|
793
|
-
for vs in self._ktcicks.value.split(",")
|
|
794
|
-
]
|
|
795
|
-
kticks = [(int(vs[0]), vs[1]) for vs in hsk if len(vs) == 2] or None
|
|
796
|
-
self._kwargs = {**self._kwargs, "kticks": kticks, # below numbers instead of index and full shown range
|
|
797
|
-
"bands": range(self._brange.value[0] - 1, self._brange.value[1]) if self._brange.value else None}
|
|
798
|
-
|
|
799
|
-
if self._ppicks.projections:
|
|
800
|
-
self._kwargs = {"projections": self._ppicks.projections, **self._kwargs}
|
|
801
|
-
fig = self.bands.iplot_rgb_lines(**self._kwargs, name="Up")
|
|
802
|
-
if self.bands.source.summary.ISPIN == 2:
|
|
803
|
-
self.bands.iplot_rgb_lines(**self._kwargs, spin=1, name="Down", fig=fig)
|
|
804
|
-
|
|
805
|
-
self.iplot = partial(self.bands.iplot_rgb_lines, **self._kwargs)
|
|
806
|
-
self.splot = partial(self.bands.splot_rgb_lines, **self._kwargs)
|
|
807
|
-
else:
|
|
808
|
-
fig = self.bands.iplot_bands(**self._kwargs, name="Up")
|
|
809
|
-
if self.bands.source.summary.ISPIN == 2:
|
|
810
|
-
self.bands.iplot_bands(**self._kwargs, spin=1, name="Down", fig=fig)
|
|
811
|
-
|
|
812
|
-
self.iplot = partial(self.bands.iplot_bands, **self._kwargs)
|
|
813
|
-
self.splot = partial(self.bands.splot_bands, **self._kwargs)
|
|
814
|
-
|
|
815
|
-
ptk.iplot2widget(fig, self._fig, template=self._tsd.value)
|
|
816
|
-
self._click_dict.clear() # avoid data from previous figure
|
|
817
|
-
self._select_dict.clear() # avoid data from previous figure
|
|
818
|
-
store_clicked_data(
|
|
819
|
-
self._fig,
|
|
820
|
-
self._click_dict,
|
|
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
|
-
)
|
|
751
|
+
self.iplot = partial(self.bands.iplot_bands, **self._kws)
|
|
752
|
+
self.splot = partial(self.bands.splot_bands, **self._kws)
|
|
847
753
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
#
|
|
851
|
-
|
|
852
|
-
|
|
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
|
|
758
|
+
|
|
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)
|
|
853
763
|
|
|
854
764
|
data_dict = self._result.copy() # Copy old data
|
|
855
765
|
|
|
856
|
-
if
|
|
857
|
-
x = round(
|
|
858
|
-
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
|
|
859
769
|
|
|
860
|
-
if key := self.
|
|
770
|
+
if key := self.params.cpoint.value:
|
|
861
771
|
data_dict[key] = y # Assign value back
|
|
862
|
-
if not key.startswith("
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
) # 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
|
|
866
776
|
|
|
867
777
|
if data_dict.get("vbm", None) and data_dict.get("cbm", None):
|
|
868
778
|
data_dict["gap"] = np.round(data_dict["cbm"] - data_dict["vbm"], 6)
|
|
@@ -873,22 +783,51 @@ class BandsWidget(VBox):
|
|
|
873
783
|
)
|
|
874
784
|
|
|
875
785
|
self._result.update(data_dict) # store new data
|
|
876
|
-
_show_and_save(self._result)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
+
|
|
884
805
|
@property
|
|
885
806
|
def results(self):
|
|
886
807
|
"Generate a dataframe form result.json file in each folder."
|
|
887
|
-
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
|
+
|
|
888
827
|
|
|
889
828
|
|
|
890
829
|
@fix_signature
|
|
891
|
-
class
|
|
830
|
+
class KPathWidget(_ThemedFigureInteract):
|
|
892
831
|
"""
|
|
893
832
|
Interactively bulid a kpath for bandstructure calculation.
|
|
894
833
|
|
|
@@ -900,159 +839,163 @@ class KpathWidget(VBox):
|
|
|
900
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.
|
|
901
840
|
- To break the path between two points "Γ" and "X" type "Γ 0,X" in the "Labels" box, zero means no points in interval.
|
|
902
841
|
|
|
903
|
-
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.
|
|
904
844
|
"""
|
|
845
|
+
file = traitlets.Any(None, allow_none=True)
|
|
905
846
|
|
|
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")
|
|
847
|
+
@property
|
|
848
|
+
def poscar(self): return self._poscar
|
|
849
|
+
|
|
850
|
+
def __init__(self, files, height="450px"):
|
|
851
|
+
self.add_class("KPathWidget")
|
|
915
852
|
self._poscar = None
|
|
916
|
-
self.
|
|
853
|
+
self._oldclick = None
|
|
917
854
|
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
|
|
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
|
|
931
865
|
)
|
|
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
|
-
|
|
939
|
-
@property
|
|
940
|
-
def path(self):
|
|
941
|
-
"Returns currently selected path."
|
|
942
|
-
return self._interact._dd.value # itself a Path object
|
|
943
|
-
|
|
944
|
-
@property
|
|
945
|
-
def files(self):
|
|
946
|
-
"Use slef.files.update(...) to keep state of widget preserved."
|
|
947
|
-
return self._files
|
|
948
866
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
+
)
|
|
953
883
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
884
|
+
@ei.callback('out-fig')
|
|
885
|
+
def _update_fig(self, file, fig):
|
|
886
|
+
if not file: return # empty one
|
|
957
887
|
|
|
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
|
|
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
|
|
997
922
|
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
|
-
if
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
1046
991
|
|
|
1047
992
|
def get_kpoints(self):
|
|
1048
993
|
"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
|
|
994
|
+
keys = [idx for (_, idx) in self.params.sm.options if idx in self._kpoints] # order and existence is important
|
|
1052
995
|
kpts = [self._kpoints[k] for k in keys]
|
|
1053
996
|
LN = [
|
|
1054
997
|
lab.split("⋮")[1].strip().split()
|
|
1055
|
-
for (lab, idx) in self.
|
|
998
|
+
for (lab, idx) in self.params.sm.options
|
|
1056
999
|
if idx in keys
|
|
1057
1000
|
]
|
|
1058
1001
|
|
|
@@ -1072,76 +1015,43 @@ class KpathWidget(VBox):
|
|
|
1072
1015
|
)
|
|
1073
1016
|
return kpts
|
|
1074
1017
|
|
|
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
|
|
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
|
|
1106
1035
|
|
|
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
|
|
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
|
|
1139
1045
|
|
|
1140
1046
|
@_sub_doc(lat.get_kpath, {"kpoints :.*n :": "n :", "rec_basis :.*\n\n": "\n\n"})
|
|
1141
1047
|
@_sig_kwargs(lat.get_kpath, ("kpoints", "rec_basis"))
|
|
1142
1048
|
def get_kpath(self, n=5, **kwargs):
|
|
1143
1049
|
return self.poscar.get_kpath(self.get_kpoints(), n=n, **kwargs)
|
|
1144
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
|
+
|
|
1145
1055
|
def splot(self, plane=None, fmt_label=lambda x: x, plot_kws={}, **kwargs):
|
|
1146
1056
|
"""
|
|
1147
1057
|
Same as `ipyvasp.lattice.POSCAR.splot_bz` except it also plots path on BZ.
|
|
@@ -1165,10 +1075,5 @@ class KpathWidget(VBox):
|
|
|
1165
1075
|
) # plots on ax automatically
|
|
1166
1076
|
return ax
|
|
1167
1077
|
|
|
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
1078
|
# Should be at end
|
|
1174
1079
|
del fix_signature # no more need
|