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.
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/PKG-INFO +2 -2
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/README.md +1 -1
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/_enplots.py +27 -47
- ipyvasp-0.9.95/ipyvasp/_version.py +1 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/widgets.py +212 -106
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/PKG-INFO +2 -2
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/setup.py +1 -1
- ipyvasp-0.9.93/ipyvasp/_version.py +0 -1
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/LICENSE +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/__init__.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/__main__.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/_lattice.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/bsdos.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/cli.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/__init__.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/parser.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/plot_toolkit.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/serializer.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/core/spatial_toolkit.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/evals_dataframe.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/lattice.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/misc.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/potential.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp/utils.py +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/SOURCES.txt +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/dependency_links.txt +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/entry_points.txt +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/requires.txt +0 -0
- {ipyvasp-0.9.93 → ipyvasp-0.9.95}/ipyvasp.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|

|
|
78
78
|
|
|
79
|
-
|
|
79
|
+

|
|
80
80
|
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
|
-
|
|
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["
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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,
|
|
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,
|
|
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
|
-
"
|
|
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)",
|
|
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 =
|
|
340
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
367
|
-
return
|
|
380
|
+
return
|
|
368
381
|
|
|
369
382
|
sorbs = system_summary.orbs
|
|
370
|
-
self._orbs_map = {"
|
|
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.
|
|
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.
|
|
411
|
-
self.
|
|
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
|
-
|
|
422
|
-
|
|
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
|
|
479
|
-
"
|
|
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
|
-
|
|
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,
|
|
585
|
-
- selected_data: Last selection of points within a box or lasso
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
635
|
-
projs =
|
|
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 =
|
|
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
|
|
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
|
|
740
|
-
_fig = self.bands.iplot_rgb_lines(**self._kws, name="Up")
|
|
741
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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 =
|
|
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.
|
|
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
|

|
|
78
78
|
|
|
79
|
-
|
|
79
|
+

|
|
80
80
|
More coming soon!
|
|
@@ -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
|
|
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
|