ipyvasp 0.9.93__tar.gz → 0.9.94__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.94}/PKG-INFO +1 -1
  2. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/_enplots.py +27 -47
  3. ipyvasp-0.9.94/ipyvasp/_version.py +1 -0
  4. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/widgets.py +204 -106
  5. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/PKG-INFO +1 -1
  6. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/setup.py +1 -1
  7. ipyvasp-0.9.93/ipyvasp/_version.py +0 -1
  8. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/LICENSE +0 -0
  9. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/README.md +0 -0
  10. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/__init__.py +0 -0
  11. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/__main__.py +0 -0
  12. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/_lattice.py +0 -0
  13. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/bsdos.py +0 -0
  14. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/cli.py +0 -0
  15. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/core/__init__.py +0 -0
  16. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/core/parser.py +0 -0
  17. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/core/plot_toolkit.py +0 -0
  18. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/core/serializer.py +0 -0
  19. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/core/spatial_toolkit.py +0 -0
  20. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/evals_dataframe.py +0 -0
  21. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/lattice.py +0 -0
  22. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/misc.py +0 -0
  23. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/potential.py +0 -0
  24. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp/utils.py +0 -0
  25. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/SOURCES.txt +0 -0
  26. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/dependency_links.txt +0 -0
  27. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/entry_points.txt +0 -0
  28. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/requires.txt +0 -0
  29. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/ipyvasp.egg-info/top_level.txt +0 -0
  30. {ipyvasp-0.9.93 → ipyvasp-0.9.94}/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.94
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
@@ -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.94"
@@ -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):
@@ -566,13 +629,6 @@ class _ThemedFigureInteract(ei.InteractBase):
566
629
  raise AttributeError("self._files = Files(...) was never set!")
567
630
  return self._files
568
631
 
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
632
 
577
633
  @fix_signature
578
634
  class BandsWidget(_ThemedFigureInteract):
@@ -581,23 +637,44 @@ class BandsWidget(_ThemedFigureInteract):
581
637
  You can observe three traits:
582
638
 
583
639
  - 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.
640
+ - clicked_data: Last clicked point data, which can be directly passed to a dataframe.
641
+ - selected_data: Last selection of points within a box or lasso, which can be directly passed to a dataframe and plotted accordingly.
586
642
 
587
643
  - 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.
644
+ - You can also use `self.iplot`, `self.splot` with `self.kws` to get static plts of current state, and self.results to get a dataframe.
645
+ - You can use store_clicks to provide extra names of points you want to click and save data, besides default ones.
589
646
  """
590
647
  file = traitlets.Any(allow_none=True)
591
648
  clicked_data = traitlets.Dict(allow_none=True)
592
649
  selected_data = traitlets.Dict(allow_none=True)
593
650
 
594
- def __init__(self, files, height="450px"):
651
+ def __init__(self, files, height="600px", store_clicks=None):
595
652
  self.add_class("BandsWidget")
653
+ self._kb_fig = go.FigureWidget() # for extra stuff
654
+ self._kb_fig.update_layout(margin=dict(l=40, r=0, b=40, t=40, pad=0)) # show compact
596
655
  self._files = Files(files)
597
656
  self._bands = None
598
657
  self._kws = {}
599
658
  self._result = {}
600
- super().__init__()
659
+ self._extra_clicks = ()
660
+
661
+ if store_clicks is not None:
662
+ if not isinstance(store_clicks, (list,tuple)):
663
+ raise TypeError("store_clicks should be list of names "
664
+ f"of point to be stored from click on figure, got {type(store_clicks)}")
665
+
666
+ for name in store_clicks:
667
+ if not isinstance(name, str) or not name.isidentifier():
668
+ raise ValueError(f"items in store_clicks should be a valid python variable name, got {name!r}")
669
+ if name in ["vbm", "cbm", "so_max", "so_min"]:
670
+ raise ValueError(f"{name!r} already exists in default click points!")
671
+ reserved = "gap soc v a b c alpha beta gamma direct".split()
672
+ if name in reserved:
673
+ raise ValueError(f"{name!r} conflicts with reserved keys {reserved}")
674
+
675
+ self._extra_clicks += tuple(store_clicks)
676
+
677
+ super().__init__() # after extra clicks
601
678
 
602
679
  traitlets.dlink((self.params.file,'value'),(self, 'file'))
603
680
  traitlets.dlink((self.params.fig,'clicked'),(self, 'clicked_data'))
@@ -606,7 +683,7 @@ class BandsWidget(_ThemedFigureInteract):
606
683
  self.relayout(
607
684
  left_sidebar=[
608
685
  'head','file','krange','kticks','brange', 'ppicks',
609
- [HBox(),('theme','button')],
686
+ [HBox(),('theme','button')], 'kb_fig',
610
687
  ],
611
688
  center=['hdata','fig','cpoint'], footer = self.groups.outputs,
612
689
  right_sidebar = ['showft'],
@@ -614,13 +691,29 @@ class BandsWidget(_ThemedFigureInteract):
614
691
  height=height
615
692
  )
616
693
 
694
+ @traitlets.validate('selected_data','clicked_data')
695
+ def _flatten_dict(self, proposal):
696
+ data = proposal['value']
697
+ if data is None: return None # allow None stuff
698
+
699
+ if not isinstance(data, dict):
700
+ raise traitlets.TraitError(f"Expected a dict for selected_data, got {type(data)}")
701
+
702
+ _data = {k:v for k,v in data.items() if k != 'customdata' and 'indexes' not in k}
703
+ _data.update(pd.DataFrame(data.get('customdata',{})).to_dict(orient='list'))
704
+ return _data # since we know customdata, we can flatten dict
705
+
706
+
617
707
  @ei.callback
618
708
  def _update_theme(self, fig, theme):
619
- return super()._update_theme(fig, theme)
709
+ super()._update_theme(fig, theme)
710
+ self._kb_fig.layout.template = fig.layout.template
711
+ self._kb_fig.layout.autosize = True
620
712
 
621
713
  def _interactive_params(self):
622
714
  return dict(
623
715
  fig = self._fig, theme = self._theme, # include theme and fig
716
+ kb_fig = self._kb_fig, # show selected data
624
717
  head = ipw.HTML("<b>Band Structure Visualizer</b>"),
625
718
  file = self.files.to_dropdown(),
626
719
  ppicks = PropsPicker(),
@@ -629,13 +722,39 @@ class BandsWidget(_ThemedFigureInteract):
629
722
  kticks = Text(description="kticks", tooltip="0 index maps to minimum value of kpoints slider."),
630
723
  brange = ipw.IntRangeSlider(description="bands",min=1, max=1), # number, not index
631
724
  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
725
+ value=None, options=["vbm", "cbm", *self._extra_clicks]).add_class('content-width-button'), # the point where clicked
726
+ showft = ipw.IntSlider(description = 'h', orientation='vertical',min=0,max=50, value=0,tooltip="outputs area's height ratio"),
727
+ cdata = 'fig.clicked',
728
+ projs = 'ppicks.projections', # for visual feedback on button
729
+ sdata = '.selected_data',
636
730
  hdata = ipw.HTML(), # to show data in one place
637
731
  )
638
732
 
733
+ @ei.callback('out-selected')
734
+ def _plot_data(self, kb_fig, sdata):
735
+ kb_fig.data = [] # clear in any case to avoid confusion
736
+ if not sdata: return # no change
737
+
738
+ df = pd.DataFrame(sdata)
739
+ if 'r' in sdata:
740
+ arr = df[['r','g','b']].to_numpy()
741
+ arr[arr == ''] = 0
742
+ arr, fmt = arr / (arr.max() or 1), lambda v : int(v*255) # color norms
743
+ df['color'] = [f"rgb({fmt(r)},{fmt(g)},{fmt(b)})" for r,g,b in arr]
744
+ else:
745
+ df['color'] = sdata['occ']
746
+
747
+ df['msize'] = df['occ']*7 + 10
748
+ cdata = (df[["ys","occ","r","g","b"]] if 'r' in sdata else df[['ys','occ']]).to_numpy()
749
+ rgb_temp = '<br>orbs: (%{customdata[2]},%{customdata[3]},%{customdata[4]})' if 'r' in sdata else ''
750
+
751
+ kb_fig.add_trace(go.Scatter(x=df.nk, y = df.nb, mode = 'markers', marker = dict(size=df.msize,color=df.color), customdata=cdata))
752
+ kb_fig.update_traces(hovertemplate=f"nk: %{{x}}, nb: %{{y}})<br>en: %{{customdata[0]:.4f}}<br>occ: %{{customdata[1]:.4f}}{rgb_temp}<extra></extra>")
753
+ kb_fig.update_layout(template = self._fig.layout.template, autosize=True,
754
+ title = "Selected Data", showlegend=False,coloraxis_showscale=False,
755
+ margin=dict(l=40, r=0, b=40, t=40, pad=0),font=dict(family="stix, serif", size=14)
756
+ )
757
+
639
758
  @ei.callback('out-data')
640
759
  def _load_data(self, file):
641
760
  if not file: return # First time not available
@@ -654,11 +773,11 @@ class BandsWidget(_ThemedFigureInteract):
654
773
 
655
774
  self.params.brange.max = self.bands.source.summary.NBANDS
656
775
  if self.bands.source.summary.LSORBIT:
657
- self.params.cpoint.options = ["vbm", "cbm", "so_max", "so_min"]
776
+ self.params.cpoint.options = ["vbm", "cbm", "so_max", "so_min", *self._extra_clicks]
658
777
  else:
659
- self.params.cpoint.options = ["vbm", "cbm"]
778
+ self.params.cpoint.options = ["vbm", "cbm",*self._extra_clicks]
660
779
  if (path := file.parent / "result.json").is_file():
661
- self._result = self._clean_legacy_data(path)
780
+ self._result = _clean_legacy_data(path)
662
781
 
663
782
  pdata = self.bands.source.poscar.data
664
783
  self._result.update(
@@ -669,38 +788,6 @@ class BandsWidget(_ThemedFigureInteract):
669
788
  }
670
789
  )
671
790
  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
791
 
705
792
  @ei.callback
706
793
  def _toggle_footer(self, showft):
@@ -711,7 +798,7 @@ class BandsWidget(_ThemedFigureInteract):
711
798
  self._kws["kpairs"] = [krange,]
712
799
 
713
800
  @ei.callback
714
- def _warn_update(self, file=None, kticks=None, brange=None, krange=None,projs=None):
801
+ def _warn_update(self, file, kticks, brange, krange, projs):
715
802
  self.params.button.description = "🔴 Update Graph"
716
803
 
717
804
  @ei.callback('out-graph')
@@ -734,17 +821,18 @@ class BandsWidget(_ThemedFigureInteract):
734
821
  _bands = range(l-1, h) # from number to index
735
822
 
736
823
  self._kws = {**self._kws, "kticks": kticks, "bands": _bands}
737
-
824
+ ISPIN = self.bands.source.summary.ISPIN
738
825
  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:
826
+ self._kws["projections"] = self.params.ppicks.projections
827
+ _fig = self.bands.iplot_rgb_lines(**self._kws, name="Up" if ISPIN == 2 else "")
828
+ if ISPIN == 2:
742
829
  self.bands.iplot_rgb_lines(**self._kws, spin=1, name="Down", fig=fig)
743
830
 
744
831
  self.iplot = partial(self.bands.iplot_rgb_lines, **self._kws)
745
832
  self.splot = partial(self.bands.splot_rgb_lines, **self._kws)
746
833
  else:
747
- _fig = self.bands.iplot_bands(**self._kws, name="Up")
834
+ self._kws.pop("projections",None) # may be previous one
835
+ _fig = self.bands.iplot_bands(**self._kws, name="Up" if ISPIN == 2 else "")
748
836
  if self.bands.source.summary.ISPIN == 2:
749
837
  self.bands.iplot_bands(**self._kws, spin=1, name="Down", fig=fig)
750
838
 
@@ -758,21 +846,26 @@ class BandsWidget(_ThemedFigureInteract):
758
846
 
759
847
  @ei.callback('out-click')
760
848
  def _click_save_data(self, cdata):
761
- if self.params.cpoint.value is None:
762
- return self._show_and_save(self._result)
763
-
849
+ if self.params.cpoint.value is None: return # at reset-
764
850
  data_dict = self._result.copy() # Copy old data
765
851
 
766
852
  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
853
+ key = self.params.cpoint.value
854
+ if key:
855
+ y = round(float(*cdata['ys']) + self.bands.data.ezero, 6) # Add ezero
856
+ if not key in self._extra_clicks:
857
+ data_dict[key] = y # Assign value back
858
+
772
859
  if not key.startswith("so_"): # not spin-orbit points
773
860
  cst, = cdata.get('customdata',[{}]) # single item
774
861
  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
862
+ kp = tuple([round(k,6) if k else k for k in kp])
863
+
864
+ if key in ("vbm","cbm"):
865
+ data_dict[f"k{key}"] = kp
866
+ else: # user points, stor both for reference
867
+ data_dict[key] = {"k":kp,"e":y}
868
+
776
869
 
777
870
  if data_dict.get("vbm", None) and data_dict.get("cbm", None):
778
871
  data_dict["gap"] = np.round(data_dict["cbm"] - data_dict["vbm"], 6)
@@ -783,29 +876,33 @@ class BandsWidget(_ThemedFigureInteract):
783
876
  )
784
877
 
785
878
  self._result.update(data_dict) # store new data
786
- self._show_and_save(self._result)
879
+ self._show_and_save(self._result, f"{key} = {data_dict[key]}")
787
880
  self.params.cpoint.value = None # Reset to None to avoid accidental click at end
788
881
 
789
- def _show_data(self, data):
882
+ def _show_data(self, data, last_click=None):
790
883
  "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)
884
+ keys = "sys vbm cbm gap direct soc v a b c alpha beta gamma".split()
885
+ data = {key:data[key] for key in keys if key in data} # show only standard data
886
+ kv, kc = [self._result.get(k,[None]*3) for k in ('kvbm','kcbm')]
793
887
  data['direct'] = (kv == kc) if None not in kv else False
888
+
889
+ # Add a caption to the table
890
+ caption = f"<caption style='caption-side:bottom; opacity:0.7;'><code>{last_click or 'clicked data is shown here'}</code></caption>"
891
+
794
892
  headers = "".join(f"<th>{key}</th>" for key in data.keys())
795
893
  values = "".join(f"<td>{format(value, '.4f') if isinstance(value, float) else value}</td>" for value in data.values())
796
894
  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>"""
895
+ {caption}<tr>{headers}</tr>\n<tr>{values}</tr></table>"""
798
896
 
799
- def _show_and_save(self, data_dict):
800
- self._show_data(data_dict)
897
+ def _show_and_save(self, data_dict, last_click=None):
898
+ self._show_data(data_dict,last_click=last_click)
801
899
  if self.file:
802
900
  serializer.dump(data_dict,format="json",
803
901
  outfile=self.file.parent / "result.json")
804
902
 
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)
903
+ def results(self, exclude_keys=None):
904
+ "Generate a dataframe form result.json file in each folder, with optionally excluding keys."
905
+ return load_results(self.params.file.options, exclude_keys=exclude_keys)
809
906
 
810
907
  @property
811
908
  def source(self):
@@ -876,7 +973,7 @@ class KPathWidget(_ThemedFigureInteract):
876
973
  lab = Text(description="Labels", continuous_update=True),
877
974
  kpt = Text(description="KPOINT", continuous_update=False),
878
975
  delp = Button(description=" ", icon='trash', tooltip="Delete Selected Points"),
879
- click = {'fig': 'clicked'},
976
+ click = 'fig.clicked',
880
977
  lock = Button(description=" ", icon='unlock', tooltip="Lock/Unlock adding more points"),
881
978
  info = ipw.HTML(), # consise information in one place
882
979
  )
@@ -935,6 +1032,7 @@ class KPathWidget(_ThemedFigureInteract):
935
1032
  def _update_theme(self, fig, theme):
936
1033
  super()._update_theme(fig, theme) # call parent method, but important
937
1034
 
1035
+
938
1036
  @ei.callback
939
1037
  def _toggle_lock(self, lock):
940
1038
  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.94
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
@@ -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
File without changes