ipyvasp 0.9.93__tar.gz → 0.9.95__tar.gz

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.
Files changed (30) hide show
  1. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/PKG-INFO +2 -2
  2. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/README.md +1 -1
  3. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/_enplots.py +27 -47
  4. ipyvasp-0.9.95/ipyvasp/_version.py +1 -0
  5. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/widgets.py +212 -106
  6. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/PKG-INFO +2 -2
  7. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/setup.py +1 -1
  8. ipyvasp-0.9.93/ipyvasp/_version.py +0 -1
  9. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/LICENSE +0 -0
  10. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/__init__.py +0 -0
  11. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/__main__.py +0 -0
  12. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/_lattice.py +0 -0
  13. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/bsdos.py +0 -0
  14. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/cli.py +0 -0
  15. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/__init__.py +0 -0
  16. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/parser.py +0 -0
  17. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/plot_toolkit.py +0 -0
  18. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/serializer.py +0 -0
  19. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/spatial_toolkit.py +0 -0
  20. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/evals_dataframe.py +0 -0
  21. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/lattice.py +0 -0
  22. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/misc.py +0 -0
  23. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/potential.py +0 -0
  24. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/utils.py +0 -0
  25. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/SOURCES.txt +0 -0
  26. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/dependency_links.txt +0 -0
  27. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/entry_points.txt +0 -0
  28. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/requires.txt +0 -0
  29. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/top_level.txt +0 -0
  30. {ipyvasp-0.9.93 → ipyvasp-0.9.95}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipyvasp
3
- Version: 0.9.93
3
+ Version: 0.9.95
4
4
  Summary: A processing tool for VASP DFT input/output processing in Jupyter Notebook.
5
5
  Home-page: https://github.com/massgh/ipyvasp
6
6
  Author: Abdul Saboor
@@ -76,5 +76,5 @@ Apply operations on POSCAR and simultaneously view using plotly's `FigureWidget`
76
76
 
77
77
  ![snip](op.png)
78
78
 
79
-
79
+ ![BandsWidget](Bands.jpg)
80
80
  More coming soon!
@@ -43,5 +43,5 @@ Apply operations on POSCAR and simultaneously view using plotly's `FigureWidget`
43
43
 
44
44
  ![snip](op.png)
45
45
 
46
-
46
+ ![BandsWidget](Bands.jpg)
47
47
  More coming soon!
@@ -716,8 +716,9 @@ def splot_dos_lines(
716
716
  **legend_kws,
717
717
  }
718
718
  add_legend(ax, **kwargs) # Labels are picked from plot
719
-
720
- kws = dict(ylim=elim or []) if vertical else dict(xlim=elim or [])
719
+
720
+ elim = elim if elim is not None else []
721
+ kws = dict(ylim=elim) if vertical else dict(xlim=elim)
721
722
  xlabel, ylabel = "Energy (eV)", "DOS"
722
723
  if vertical:
723
724
  xlabel, ylabel = ylabel, xlabel
@@ -754,7 +755,7 @@ def _format_rgb_data(
754
755
  if data["pros"].shape[2] == 2:
755
756
  data["norms"][:, :, 2] = np.nan # Avoid wrong info here
756
757
  elif data["pros"].shape[2] == 1:
757
- data["pros"][:, :, 1:] = np.nan
758
+ data["norms"][:, :, 1:] = np.nan
758
759
 
759
760
  lws = np.sum(rgb, axis=2) # Sum of all colors
760
761
  lws = maxwidth * lws / (float(np.max(lws)) or 1) # Normalize to maxwidth
@@ -773,8 +774,7 @@ def _format_rgb_data(
773
774
  indices = range(np.shape(data["evals"])[1])
774
775
 
775
776
  # Now process data to make single data for faster plotting.
776
- txt = "Projection: [{}]</br>Value:".format(", ".join(labels))
777
- K, E, C, S, PT, OT, KT, ET, jKbop = [], [], [], [], [], [], [], [], []
777
+ K, E, C, S, CDATA = [], [], [], [], []
778
778
  for i, b in enumerate(indices):
779
779
  K = [*K, *data["kpath"], np.nan]
780
780
  E = [*E, *data["evals"][:, i], np.nan]
@@ -784,46 +784,26 @@ def _format_rgb_data(
784
784
  "rgb(0,0,0)",
785
785
  ]
786
786
  S = [*S, *data["widths"][:, i], data["widths"][-1, i]]
787
- PT = [*PT, *[f"{txt} [{s}, {p}, {d}]" for (s, p, d) in data["norms"][:, i]], ""]
788
- OT = [*OT, *[f"Occ: {t:>7.4f}" for t in data["occs"][:, i]], ""]
789
- KT = [
790
- *KT,
791
- *[
792
- f"K<sub>{j+1}</sub>: {x:>7.3f}{y:>7.3f}{z:>7.3f}"
793
- for j, (x, y, z) in enumerate(data["kpoints"])
794
- ],
795
- "",
796
- ]
797
- ET = [
798
- *ET,
799
- *["{}".format(b + 1) for _ in data["kpath"]],
800
- "",
801
- ] # Add bands subscripts to labels.
802
787
 
803
- jKbop = [*jKbop, *[
788
+ CDATA = [*CDATA , *[
804
789
  {
805
790
  "nk":j+1,
806
791
  **{f"k{u}":v for u,v in zip("xyz",xyz)},
807
792
  "nb":b+1,
808
793
  "occ":occ,
809
- **{c:v for c,v in zip("rgb",rgb)}
794
+ **{c:"" if np.isnan(v) else v for c,v in zip("rgb",rgb)}
810
795
  }
811
- for (j, xyz), occ,rgb in zip(
796
+ for (j, xyz), occ, rgb in zip(
812
797
  enumerate(data["kpoints"]), data["occs"][:, i],data["norms"][:, i]
813
798
  )
814
799
  ], {k:np.nan for k in ("nk","kx","ky","kz","nb","occ","r","g","b")}]
815
800
 
816
- T = [
817
- f"</br>{p} </br></br>Band: {e} {o}</br>{k}"
818
- for (p, e, o, k) in zip(PT, ET, OT, KT)
819
- ]
820
801
  return {
821
802
  "K": K,
822
803
  "E": E,
823
804
  "C": C,
824
805
  "S": S,
825
- "T": T,
826
- "jKbop": jKbop,
806
+ "CDATA": CDATA,
827
807
  "labels": labels,
828
808
  } # K, energy, marker color, marker size, text, labels that get changed
829
809
 
@@ -844,6 +824,11 @@ def _fmt_labels(ticklabels):
844
824
  ]
845
825
  return ticklabels
846
826
 
827
+ _hover_temp = { # keep order same
828
+ "xy":"(%{x}, %{y})",
829
+ "k": "<br>K<sub>%{customdata.nk}</sub>: %{customdata.kx:.3f} %{customdata.ky:.3f} %{customdata.kz:.3f}",
830
+ "b":"Band: %{customdata.nb}, Occ: %{customdata.occ:.4f}"
831
+ }
847
832
 
848
833
  @gu._fmt_doc(_docs)
849
834
  def iplot_bands(
@@ -876,10 +861,7 @@ def iplot_bands(
876
861
  maxwidth=1,
877
862
  indices=indices,
878
863
  ) # moking other arrays, we need only
879
- K, E, T = data["K"], data["E"], data["T"] # Fixed K and E as single line data
880
- T = [
881
- "Band" + t.split("Band")[1].split("Occ")[0] for t in T
882
- ] # Just Band number here
864
+ K, E = data["K"], data["E"]
883
865
 
884
866
  if fig is None:
885
867
  fig = go.Figure()
@@ -887,11 +869,12 @@ def iplot_bands(
887
869
  kwargs = {
888
870
  "mode": "markers + lines",
889
871
  "marker": dict(size=0.1),
890
- "customdata": [{k:v for k,v in d.items() if not k in 'rgb'} for d in data["jKbop"]], # useless rgb data to skip
872
+ "hovertemplate": "<br>".join(_hover_temp.values()),
873
+ "customdata": [{k:v for k,v in d.items() if not k in 'rgb'} for d in data["CDATA"]], # useless rgb data to skip
891
874
  **kwargs,
892
875
  } # marker so that it is selectable by box, otherwise it does not
893
- fig.add_trace(go.Scatter(x=K, y=E, hovertext=T, **kwargs))
894
-
876
+ fig.add_trace(go.Scatter(x=K, y=E, **kwargs))
877
+
895
878
  fig.update_layout(
896
879
  template="plotly_white",
897
880
  title=(
@@ -947,14 +930,7 @@ def iplot_rgb_lines(
947
930
  data = _format_rgb_data(
948
931
  K, E, pros, labels, interp, occs, kpoints, maxwidth=maxwidth, indices=indices
949
932
  )
950
- K, E, C, S, T, labels = (
951
- data["K"],
952
- data["E"],
953
- data["C"],
954
- data["S"],
955
- data["T"],
956
- data["labels"],
957
- )
933
+ K, E, C, S, labels = [data[key] for key in "K E C S labels".split()]
958
934
 
959
935
  if fig is None:
960
936
  fig = go.Figure()
@@ -963,14 +939,18 @@ def iplot_rgb_lines(
963
939
  kwargs.pop("marker_size", None) # Provided by S
964
940
  kwargs.update(
965
941
  {
966
- "hovertext": T,
967
942
  "marker": {
968
943
  "line_color": "rgba(0,0,0,0)",
969
944
  **kwargs.get("marker", {}),
970
945
  "color": C,
971
946
  "size": S,
972
947
  },
973
- "customdata": data["jKbop"], # need for selection and hover template
948
+ "hovertemplate": "<br>".join([_hover_temp["xy"],
949
+ "<br>Projection: [{}, {}, {}]".format(*labels), # clean labels instead of ''
950
+ "Value: [%{customdata.r}, %{customdata.g}, %{customdata.b}]",
951
+ _hover_temp["k"], _hover_temp["b"],
952
+ ]),
953
+ "customdata": data["CDATA"], # need for selection and hover template
974
954
  }
975
955
  ) # marker edge should be free
976
956
 
@@ -983,7 +963,7 @@ def iplot_rgb_lines(
983
963
  + ", ".join(labels)
984
964
  + "]", # Do not set autosize = False, need to be responsive in widgets boxes
985
965
  margin=go.layout.Margin(l=60, r=50, b=40, t=75, pad=0),
986
- yaxis=go.layout.YAxis(title_text="Energy (eV)", range=elim or [min(E), max(E)]),
966
+ yaxis=go.layout.YAxis(title_text="Energy (eV)",range=elim or [min(E), max(E)]),
987
967
  xaxis=go.layout.XAxis(
988
968
  ticktext=_fmt_labels(xticklabels),
989
969
  tickvals=xticks,
@@ -0,0 +1 @@
1
+ __version__ = "0.9.95"
@@ -24,6 +24,7 @@ from ipywidgets import (
24
24
  Text,
25
25
  Stack,
26
26
  SelectMultiple,
27
+ TagsInput,
27
28
  )
28
29
 
29
30
  # More imports
@@ -182,9 +183,9 @@ class Files:
182
183
  "Apply a func(path) -> dict and create a dataframe."
183
184
  return summarize(self._files,func, **kwargs)
184
185
 
185
- def load_results(self):
186
- "Load result.json files from these paths into a dataframe."
187
- return load_results(self._files)
186
+ def load_results(self,exclude_keys=None):
187
+ "Load result.json files from these paths into a dataframe, with optionally excluding keys."
188
+ return load_results(self._files,exclude_keys=exclude_keys)
188
189
 
189
190
  def input_info(self, *tags):
190
191
  "Grab input information into a dataframe from POSCAR and INCAR. Provide INCAR tags (case-insinsitive) to select only few of them."
@@ -328,7 +329,6 @@ class Files:
328
329
  return self.summarize(lambda path: {"size": get_file_size(path)})
329
330
 
330
331
 
331
-
332
332
  @fix_signature
333
333
  class _PropPicker(VBox):
334
334
  """Single projection picker with atoms and orbitals selection"""
@@ -336,9 +336,12 @@ class _PropPicker(VBox):
336
336
 
337
337
  def __init__(self, system_summary=None):
338
338
  super().__init__()
339
- self._atoms = Dropdown(description="Atoms")
340
- self._orbs = Dropdown(description="Orbs")
339
+ self._atoms = TagsInput(description="Atoms", allowed_tags=[],
340
+ placeholder="Select atoms", allow_duplicates = False).add_class('props-tags')
341
+ self._orbs = TagsInput(description="Orbs", allowed_tags=[],
342
+ placeholder="Select orbitals", allow_duplicates = False).add_class('props-tags')
341
343
  self.children = [self._atoms, self._orbs]
344
+ self.layout.width = '100%' # avoid horizontal collapse
342
345
  self._atoms_map = {}
343
346
  self._orbs_map = {}
344
347
 
@@ -349,13 +352,24 @@ class _PropPicker(VBox):
349
352
 
350
353
  def _update_props(self, change):
351
354
  """Update props trait when selections change"""
352
- atoms = self._atoms_map.get(self._atoms.value, [])
353
- orbs = self._orbs_map.get(self._orbs.value, [])
355
+ _atoms = [self._atoms_map.get(tag, None) for tag in self._atoms.value]
356
+ _orbs = [self._orbs_map.get(tag, None) for tag in self._orbs.value]
354
357
 
358
+ # Filter out None values, and flatten
359
+ # Flatten and filter atoms
360
+ atoms = []
361
+ for ats in _atoms:
362
+ atoms.extend(ats if ats is not None else [])
363
+
364
+ # Flatten and filter orbitals
365
+ orbs = []
366
+ for ors in _orbs:
367
+ orbs.extend(ors if ors is not None else [])
368
+
355
369
  if atoms and orbs:
356
- self.props = {
357
- 'atoms': atoms, 'orbs': orbs,
358
- 'label': f"{self._atoms.value or ''}-{self._orbs.value or ''}"
370
+ self.props = {
371
+ 'atoms': atoms, 'orbs': orbs,
372
+ 'label': f"{'+'.join(self._atoms.value)} | {'+'.join(self._orbs.value)}"
359
373
  }
360
374
  else:
361
375
  self.props = {}
@@ -363,11 +377,10 @@ class _PropPicker(VBox):
363
377
  def _process(self, system_summary):
364
378
  """Process system data and setup widget options"""
365
379
  if system_summary is None or not hasattr(system_summary, "orbs"):
366
- self.children = [ipw.HTML("❌ No projection data found!")]
367
- return
380
+ return
368
381
 
369
382
  sorbs = system_summary.orbs
370
- self._orbs_map = {"-": [], "All": range(len(sorbs)), "s": [0]}
383
+ self._orbs_map = {"All": range(len(sorbs)), "s": [0]}
371
384
 
372
385
  # p-orbitals
373
386
  if set(["px", "py", "pz"]).issubset(sorbs):
@@ -397,19 +410,17 @@ class _PropPicker(VBox):
397
410
  k: [idx] for idx, k in enumerate(sorbs[16:], start=16)
398
411
  })
399
412
 
400
- self._orbs.options = list(self._orbs_map.keys())
413
+ self._orbs.allowed_tags = list(self._orbs_map.keys())
401
414
 
402
415
  # Process atoms
403
416
  self._atoms_map = {
404
- "-": [],
405
417
  "All": range(system_summary.NIONS),
406
418
  **{k: v for k,v in system_summary.types.to_dict().items()},
407
419
  **{f"{k}{n}": [v] for k,tp in system_summary.types.to_dict().items()
408
420
  for n,v in enumerate(tp, 1)}
409
421
  }
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
422
+ self._atoms.allowed_tags = list(self._atoms_map.keys())
423
+ self._update_props(None) # Trigger props update
413
424
 
414
425
  def update(self, system_summary):
415
426
  """Update widget with new system data while preserving selections"""
@@ -418,10 +429,8 @@ class _PropPicker(VBox):
418
429
  self._process(system_summary)
419
430
 
420
431
  # 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
432
+ self._atoms.value = [tag for tag in old_atoms if tag in self._atoms.allowed_tags]
433
+ self._orbs.value = [tag for tag in old_orbs if tag in self._orbs.allowed_tags]
425
434
 
426
435
  @fix_signature
427
436
  class PropsPicker(VBox): # NOTE: remove New Later
@@ -475,8 +484,44 @@ class PropsPicker(VBox): # NOTE: remove New Later
475
484
  for picker in self._pickers:
476
485
  picker.update(system_summary)
477
486
 
478
- def load_results(paths_list):
479
- "Loads result.json from paths_list and returns a dataframe."
487
+ def _clean_legacy_data(path):
488
+ "clean old style keys like VBM to vbm"
489
+ data = serializer.load(path.absolute()) # Old data loaded
490
+ if not any(key in data for key in ['VBM', 'α','vbm_k']):
491
+ return data # already clean
492
+
493
+ keys_map = {
494
+ "SYSTEM": "sys",
495
+ "VBM": "vbm", # Old: New
496
+ "CBM": "cbm",
497
+ "VBM_k": "kvbm", "vbm_k": "kvbm",
498
+ "CBM_k": "kcbm", "cbm_k": "kcbm",
499
+ "E_gap": "gap",
500
+ "\u0394_SO": "soc",
501
+ "α": "alpha",
502
+ "β": "beta",
503
+ "γ": "gamma",
504
+ }
505
+ new_data = {k:v for k,v in data.items() if k not in (*keys_map.keys(),*keys_map.values())} # keep other data
506
+ for old, new in keys_map.items():
507
+ if old in data:
508
+ new_data[new] = data[old] # Transfer value from old key to new key
509
+ elif new in data:
510
+ new_data[new] = data[new] # Keep existing new style keys
511
+
512
+ # save cleaned data
513
+ serializer.dump(new_data,format="json",outfile=path)
514
+ return new_data
515
+
516
+
517
+ def load_results(paths_list, exclude_keys=None):
518
+ "Loads result.json from paths_list and returns a dataframe. Use exclude_keys to get subset of data."
519
+ if exclude_keys is not None:
520
+ if not isinstance(exclude_keys, (list,tuple)):
521
+ raise TypeError(f"exclude_keys should be list of keys, got {type(exclude_keys)}")
522
+ if not all([isinstance(key,str) for key in exclude_keys]):
523
+ raise TypeError(f"all keys in exclude_keys should be str!")
524
+
480
525
  paths_list = [Path(p) for p in paths_list]
481
526
  result_paths = []
482
527
  if paths_list:
@@ -488,7 +533,8 @@ def load_results(paths_list):
488
533
 
489
534
  def load_data(path):
490
535
  try:
491
- return serializer.load(str(path.absolute()))
536
+ data = _clean_legacy_data(path)
537
+ return {k:v for k,v in data.items() if k not in (exclude_keys or [])}
492
538
  except:
493
539
  return {} # If not found, return empty dictionary
494
540
 
@@ -519,6 +565,23 @@ def _get_css(mode):
519
565
  },
520
566
  '.footer': {'overflow': 'auto','padding':0},
521
567
  '.widget-vslider, .jupyter-widget-vslider': {'width': 'auto'}, # otherwise it spans too much area
568
+ 'table': { # dataframe display sucks
569
+ 'color':'var(--jp-content-font-color1)',
570
+ 'background':'var(--jp-layout-color1)',
571
+ 'tr': {
572
+ '^:nth-child(odd)': {'background':'var(--jp-widgets-input-background-color)',},
573
+ '^:nth-child(even)': {'background':'var(--jp-layout-color1)',},
574
+ },
575
+ },
576
+ '.props-picker': {
577
+ 'background': 'var(--jp-widgets-input-background-color)', # make feels like single widget
578
+ 'overflow-x': 'hidden', 'border-radius': '4px', 'padding': '4px',
579
+ },
580
+ '.props-tags': {
581
+ 'background':'var(--jp-layout-color1)', 'border-radius': '4px', 'padding': '4px',
582
+ '> input': {'width': '100%'},
583
+ '> input::placeholder': {'color': 'var(--jp-ui-font-color1)'},
584
+ },
522
585
  }
523
586
 
524
587
  class _ThemedFigureInteract(ei.InteractBase):
@@ -532,6 +595,14 @@ class _ThemedFigureInteract(ei.InteractBase):
532
595
  raise AttributeError("subclass must include already initialized "
533
596
  "{'fig': self._fig,'theme':self._theme} in returned dict of _interactive_params() method.")
534
597
  self._update_theme(self._fig,self._theme) # fix theme in starts
598
+ self.observe(self._autosize_figs, names = 'isfullscreen') # fix figurewidget problem
599
+
600
+ def _autosize_figs(self, change):
601
+ for w in self._all_widgets.values():
602
+ # don't know yet about these without importing
603
+ if re.search('plotly.*FigureWidget', str(type(w).__mro__)):
604
+ w.layout.autosize = False # Double trigger is important
605
+ w.layout.autosize = True
535
606
 
536
607
  def _interactive_params(self): return {}
537
608
 
@@ -566,13 +637,6 @@ class _ThemedFigureInteract(ei.InteractBase):
566
637
  raise AttributeError("self._files = Files(...) was never set!")
567
638
  return self._files
568
639
 
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)
576
640
 
577
641
  @fix_signature
578
642
  class BandsWidget(_ThemedFigureInteract):
@@ -581,23 +645,44 @@ class BandsWidget(_ThemedFigureInteract):
581
645
  You can observe three traits:
582
646
 
583
647
  - 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.
648
+ - clicked_data: Last clicked point data, which can be directly passed to a dataframe.
649
+ - selected_data: Last selection of points within a box or lasso, which can be directly passed to a dataframe and plotted accordingly.
586
650
 
587
651
  - 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.
652
+ - You can also use `self.iplot`, `self.splot` with `self.kws` to get static plts of current state, and self.results to get a dataframe.
653
+ - You can use store_clicks to provide extra names of points you want to click and save data, besides default ones.
589
654
  """
590
655
  file = traitlets.Any(allow_none=True)
591
656
  clicked_data = traitlets.Dict(allow_none=True)
592
657
  selected_data = traitlets.Dict(allow_none=True)
593
658
 
594
- def __init__(self, files, height="450px"):
659
+ def __init__(self, files, height="600px", store_clicks=None):
595
660
  self.add_class("BandsWidget")
661
+ self._kb_fig = go.FigureWidget() # for extra stuff
662
+ self._kb_fig.update_layout(margin=dict(l=40, r=0, b=40, t=40, pad=0)) # show compact
596
663
  self._files = Files(files)
597
664
  self._bands = None
598
665
  self._kws = {}
599
666
  self._result = {}
600
- super().__init__()
667
+ self._extra_clicks = ()
668
+
669
+ if store_clicks is not None:
670
+ if not isinstance(store_clicks, (list,tuple)):
671
+ raise TypeError("store_clicks should be list of names "
672
+ f"of point to be stored from click on figure, got {type(store_clicks)}")
673
+
674
+ for name in store_clicks:
675
+ if not isinstance(name, str) or not name.isidentifier():
676
+ raise ValueError(f"items in store_clicks should be a valid python variable name, got {name!r}")
677
+ if name in ["vbm", "cbm", "so_max", "so_min"]:
678
+ raise ValueError(f"{name!r} already exists in default click points!")
679
+ reserved = "gap soc v a b c alpha beta gamma direct".split()
680
+ if name in reserved:
681
+ raise ValueError(f"{name!r} conflicts with reserved keys {reserved}")
682
+
683
+ self._extra_clicks += tuple(store_clicks)
684
+
685
+ super().__init__() # after extra clicks
601
686
 
602
687
  traitlets.dlink((self.params.file,'value'),(self, 'file'))
603
688
  traitlets.dlink((self.params.fig,'clicked'),(self, 'clicked_data'))
@@ -606,7 +691,7 @@ class BandsWidget(_ThemedFigureInteract):
606
691
  self.relayout(
607
692
  left_sidebar=[
608
693
  'head','file','krange','kticks','brange', 'ppicks',
609
- [HBox(),('theme','button')],
694
+ [HBox(),('theme','button')], 'kb_fig',
610
695
  ],
611
696
  center=['hdata','fig','cpoint'], footer = self.groups.outputs,
612
697
  right_sidebar = ['showft'],
@@ -614,13 +699,29 @@ class BandsWidget(_ThemedFigureInteract):
614
699
  height=height
615
700
  )
616
701
 
702
+ @traitlets.validate('selected_data','clicked_data')
703
+ def _flatten_dict(self, proposal):
704
+ data = proposal['value']
705
+ if data is None: return None # allow None stuff
706
+
707
+ if not isinstance(data, dict):
708
+ raise traitlets.TraitError(f"Expected a dict for selected_data, got {type(data)}")
709
+
710
+ _data = {k:v for k,v in data.items() if k != 'customdata' and 'indexes' not in k}
711
+ _data.update(pd.DataFrame(data.get('customdata',{})).to_dict(orient='list'))
712
+ return _data # since we know customdata, we can flatten dict
713
+
714
+
617
715
  @ei.callback
618
716
  def _update_theme(self, fig, theme):
619
- return super()._update_theme(fig, theme)
717
+ super()._update_theme(fig, theme)
718
+ self._kb_fig.layout.template = fig.layout.template
719
+ self._kb_fig.layout.autosize = True
620
720
 
621
721
  def _interactive_params(self):
622
722
  return dict(
623
723
  fig = self._fig, theme = self._theme, # include theme and fig
724
+ kb_fig = self._kb_fig, # show selected data
624
725
  head = ipw.HTML("<b>Band Structure Visualizer</b>"),
625
726
  file = self.files.to_dropdown(),
626
727
  ppicks = PropsPicker(),
@@ -629,13 +730,39 @@ class BandsWidget(_ThemedFigureInteract):
629
730
  kticks = Text(description="kticks", tooltip="0 index maps to minimum value of kpoints slider."),
630
731
  brange = ipw.IntRangeSlider(description="bands",min=1, max=1), # number, not index
631
732
  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
733
+ value=None, options=["vbm", "cbm", *self._extra_clicks]).add_class('content-width-button'), # the point where clicked
734
+ showft = ipw.IntSlider(description = 'h', orientation='vertical',min=0,max=50, value=0,tooltip="outputs area's height ratio"),
735
+ cdata = 'fig.clicked',
736
+ projs = 'ppicks.projections', # for visual feedback on button
737
+ sdata = '.selected_data',
636
738
  hdata = ipw.HTML(), # to show data in one place
637
739
  )
638
740
 
741
+ @ei.callback('out-selected')
742
+ def _plot_data(self, kb_fig, sdata):
743
+ kb_fig.data = [] # clear in any case to avoid confusion
744
+ if not sdata: return # no change
745
+
746
+ df = pd.DataFrame(sdata)
747
+ if 'r' in sdata:
748
+ arr = df[['r','g','b']].to_numpy()
749
+ arr[arr == ''] = 0
750
+ arr, fmt = arr / (arr.max() or 1), lambda v : int(v*255) # color norms
751
+ df['color'] = [f"rgb({fmt(r)},{fmt(g)},{fmt(b)})" for r,g,b in arr]
752
+ else:
753
+ df['color'] = sdata['occ']
754
+
755
+ df['msize'] = df['occ']*7 + 10
756
+ cdata = (df[["ys","occ","r","g","b"]] if 'r' in sdata else df[['ys','occ']]).to_numpy()
757
+ rgb_temp = '<br>orbs: (%{customdata[2]},%{customdata[3]},%{customdata[4]})' if 'r' in sdata else ''
758
+
759
+ kb_fig.add_trace(go.Scatter(x=df.nk, y = df.nb, mode = 'markers', marker = dict(size=df.msize,color=df.color), customdata=cdata))
760
+ kb_fig.update_traces(hovertemplate=f"nk: %{{x}}, nb: %{{y}})<br>en: %{{customdata[0]:.4f}}<br>occ: %{{customdata[1]:.4f}}{rgb_temp}<extra></extra>")
761
+ kb_fig.update_layout(template = self._fig.layout.template, autosize=True,
762
+ title = "Selected Data", showlegend=False,coloraxis_showscale=False,
763
+ margin=dict(l=40, r=0, b=40, t=40, pad=0),font=dict(family="stix, serif", size=14)
764
+ )
765
+
639
766
  @ei.callback('out-data')
640
767
  def _load_data(self, file):
641
768
  if not file: return # First time not available
@@ -654,11 +781,11 @@ class BandsWidget(_ThemedFigureInteract):
654
781
 
655
782
  self.params.brange.max = self.bands.source.summary.NBANDS
656
783
  if self.bands.source.summary.LSORBIT:
657
- self.params.cpoint.options = ["vbm", "cbm", "so_max", "so_min"]
784
+ self.params.cpoint.options = ["vbm", "cbm", "so_max", "so_min", *self._extra_clicks]
658
785
  else:
659
- self.params.cpoint.options = ["vbm", "cbm"]
786
+ self.params.cpoint.options = ["vbm", "cbm",*self._extra_clicks]
660
787
  if (path := file.parent / "result.json").is_file():
661
- self._result = self._clean_legacy_data(path)
788
+ self._result = _clean_legacy_data(path)
662
789
 
663
790
  pdata = self.bands.source.poscar.data
664
791
  self._result.update(
@@ -669,38 +796,6 @@ class BandsWidget(_ThemedFigureInteract):
669
796
  }
670
797
  )
671
798
  self._show_data(self._result) # Load into view
672
-
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
704
799
 
705
800
  @ei.callback
706
801
  def _toggle_footer(self, showft):
@@ -711,7 +806,7 @@ class BandsWidget(_ThemedFigureInteract):
711
806
  self._kws["kpairs"] = [krange,]
712
807
 
713
808
  @ei.callback
714
- def _warn_update(self, file=None, kticks=None, brange=None, krange=None,projs=None):
809
+ def _warn_update(self, file, kticks, brange, krange, projs):
715
810
  self.params.button.description = "🔴 Update Graph"
716
811
 
717
812
  @ei.callback('out-graph')
@@ -734,17 +829,18 @@ class BandsWidget(_ThemedFigureInteract):
734
829
  _bands = range(l-1, h) # from number to index
735
830
 
736
831
  self._kws = {**self._kws, "kticks": kticks, "bands": _bands}
737
-
832
+ ISPIN = self.bands.source.summary.ISPIN
738
833
  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:
834
+ self._kws["projections"] = self.params.ppicks.projections
835
+ _fig = self.bands.iplot_rgb_lines(**self._kws, name="Up" if ISPIN == 2 else "")
836
+ if ISPIN == 2:
742
837
  self.bands.iplot_rgb_lines(**self._kws, spin=1, name="Down", fig=fig)
743
838
 
744
839
  self.iplot = partial(self.bands.iplot_rgb_lines, **self._kws)
745
840
  self.splot = partial(self.bands.splot_rgb_lines, **self._kws)
746
841
  else:
747
- _fig = self.bands.iplot_bands(**self._kws, name="Up")
842
+ self._kws.pop("projections",None) # may be previous one
843
+ _fig = self.bands.iplot_bands(**self._kws, name="Up" if ISPIN == 2 else "")
748
844
  if self.bands.source.summary.ISPIN == 2:
749
845
  self.bands.iplot_bands(**self._kws, spin=1, name="Down", fig=fig)
750
846
 
@@ -758,21 +854,26 @@ class BandsWidget(_ThemedFigureInteract):
758
854
 
759
855
  @ei.callback('out-click')
760
856
  def _click_save_data(self, cdata):
761
- if self.params.cpoint.value is None:
762
- return self._show_and_save(self._result)
763
-
857
+ if self.params.cpoint.value is None: return # at reset-
764
858
  data_dict = self._result.copy() # Copy old data
765
859
 
766
860
  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
769
-
770
- if key := self.params.cpoint.value:
771
- data_dict[key] = y # Assign value back
861
+ key = self.params.cpoint.value
862
+ if key:
863
+ y = round(float(*cdata['ys']) + self.bands.data.ezero, 6) # Add ezero
864
+ if not key in self._extra_clicks:
865
+ data_dict[key] = y # Assign value back
866
+
772
867
  if not key.startswith("so_"): # not spin-orbit points
773
868
  cst, = cdata.get('customdata',[{}]) # single item
774
869
  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
870
+ kp = tuple([round(k,6) if k else k for k in kp])
871
+
872
+ if key in ("vbm","cbm"):
873
+ data_dict[f"k{key}"] = kp
874
+ else: # user points, stor both for reference
875
+ data_dict[key] = {"k":kp,"e":y}
876
+
776
877
 
777
878
  if data_dict.get("vbm", None) and data_dict.get("cbm", None):
778
879
  data_dict["gap"] = np.round(data_dict["cbm"] - data_dict["vbm"], 6)
@@ -783,29 +884,33 @@ class BandsWidget(_ThemedFigureInteract):
783
884
  )
784
885
 
785
886
  self._result.update(data_dict) # store new data
786
- self._show_and_save(self._result)
887
+ self._show_and_save(self._result, f"{key} = {data_dict[key]}")
787
888
  self.params.cpoint.value = None # Reset to None to avoid accidental click at end
788
889
 
789
- def _show_data(self, data):
890
+ def _show_data(self, data, last_click=None):
790
891
  "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)
892
+ keys = "sys vbm cbm gap direct soc v a b c alpha beta gamma".split()
893
+ data = {key:data[key] for key in keys if key in data} # show only standard data
894
+ kv, kc = [self._result.get(k,[None]*3) for k in ('kvbm','kcbm')]
793
895
  data['direct'] = (kv == kc) if None not in kv else False
896
+
897
+ # Add a caption to the table
898
+ caption = f"<caption style='caption-side:bottom; opacity:0.7;'><code>{last_click or 'clicked data is shown here'}</code></caption>"
899
+
794
900
  headers = "".join(f"<th>{key}</th>" for key in data.keys())
795
901
  values = "".join(f"<td>{format(value, '.4f') if isinstance(value, float) else value}</td>" for value in data.values())
796
902
  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>"""
903
+ {caption}<tr>{headers}</tr>\n<tr>{values}</tr></table>"""
798
904
 
799
- def _show_and_save(self, data_dict):
800
- self._show_data(data_dict)
905
+ def _show_and_save(self, data_dict, last_click=None):
906
+ self._show_data(data_dict,last_click=last_click)
801
907
  if self.file:
802
908
  serializer.dump(data_dict,format="json",
803
909
  outfile=self.file.parent / "result.json")
804
910
 
805
- @property
806
- def results(self):
807
- "Generate a dataframe form result.json file in each folder."
808
- return load_results(self.params.file.options)
911
+ def results(self, exclude_keys=None):
912
+ "Generate a dataframe form result.json file in each folder, with optionally excluding keys."
913
+ return load_results(self.params.file.options, exclude_keys=exclude_keys)
809
914
 
810
915
  @property
811
916
  def source(self):
@@ -876,7 +981,7 @@ class KPathWidget(_ThemedFigureInteract):
876
981
  lab = Text(description="Labels", continuous_update=True),
877
982
  kpt = Text(description="KPOINT", continuous_update=False),
878
983
  delp = Button(description=" ", icon='trash', tooltip="Delete Selected Points"),
879
- click = {'fig': 'clicked'},
984
+ click = 'fig.clicked',
880
985
  lock = Button(description=" ", icon='unlock', tooltip="Lock/Unlock adding more points"),
881
986
  info = ipw.HTML(), # consise information in one place
882
987
  )
@@ -935,6 +1040,7 @@ class KPathWidget(_ThemedFigureInteract):
935
1040
  def _update_theme(self, fig, theme):
936
1041
  super()._update_theme(fig, theme) # call parent method, but important
937
1042
 
1043
+
938
1044
  @ei.callback
939
1045
  def _toggle_lock(self, lock):
940
1046
  self.params.lock.icon = 'lock' if self.params.lock.icon == 'unlock' else 'unlock'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipyvasp
3
- Version: 0.9.93
3
+ Version: 0.9.95
4
4
  Summary: A processing tool for VASP DFT input/output processing in Jupyter Notebook.
5
5
  Home-page: https://github.com/massgh/ipyvasp
6
6
  Author: Abdul Saboor
@@ -76,5 +76,5 @@ Apply operations on POSCAR and simultaneously view using plotly's `FigureWidget`
76
76
 
77
77
  ![snip](op.png)
78
78
 
79
-
79
+ ![BandsWidget](Bands.jpg)
80
80
  More coming soon!
@@ -28,7 +28,7 @@ REQUIRED = [
28
28
  "plotly==6.0.1",
29
29
  "requests==2.28.1",
30
30
  "typer==0.9.0",
31
- "einteract", # Any last version of einteract
31
+ "einteract", # any latest version of einteract
32
32
  ]
33
33
 
34
34
  # What packages are optional?
@@ -1 +0,0 @@
1
- __version__ = "0.9.93"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes