55import numpy as np
66import plotly .graph_objects as go
77from dash import dcc , html
8- from dash .dependencies import Component , Input , Output , State
8+ from dash .dependencies import Component , Input , Output
99from dash .exceptions import PreventUpdate
1010from frozendict import frozendict
1111from pymatgen .analysis .pourbaix_diagram import PREFAC , PourbaixDiagram
3131
3232HEIGHT = 550 # in px
3333WIDTH = 700 # in px
34+ MIN_CONCENTRATION = 1e-8
35+ MAX_CONCENTRATION = 5
3436
3537
3638class PourbaixDiagramComponent (MPComponent ):
@@ -405,50 +407,50 @@ def get_figure(
405407 )
406408 )
407409 layout .update ({"annotations" : []})
408- else :
409- # Add annotations to layout to make text more readable when displaying heatmaps
410-
411- # TODO: this doesn't work yet; resolve or scrap
412- # cmap = get_cmap(PourbaixDiagramComponent.colorscale)
413- # def get_text_color(x, y):
414- # """
415- # Set text color based on whether background at that point is dark or light.
416- # """
417- # energy = pourbaix_diagram.get_decomposition_energy(entry, pH=x, V=y)
418- # c = [int(c * 255) for c in cmap(energy)[0:3]]
419- # # borrowed from crystal_toolkit.components.structure
420- # # TODO: move to utility function and ensure correct attribution for magic numbers
421- # if 1 - (c[0] * 0.299 + c[1] * 0.587 + c[2] * 0.114) / 255 < 0.5:
422- # font_color = "#000000"
423- # else:
424- # font_color = "#ffffff"
425- # #print(energy, c, font_color)
426- # return font_color
427-
428- def get_text_size (available_vertical_space ):
429- """Set text size based on available vertical space."""
430- return min (max (6 * available_vertical_space , 12 ), 20 )
431-
432- annotations = [
433- {
434- "align" : "center" ,
435- "bgcolor" : "white" ,
436- "font" : {"color" : "black" , "size" : get_text_size (height )},
437- "opacity" : 1 ,
438- "showarrow" : False ,
439- "text" : label ,
440- "x" : x ,
441- "xanchor" : "center" ,
442- "yanchor" : "auto" ,
443- # "xshift": -10,
444- # "yshift": -10,
445- "xref" : "x" ,
446- "y" : y ,
447- "yref" : "y" ,
448- }
449- for (x , y ), label , height in zip (xy_data , labels , domain_heights )
450- ]
451- layout .update ({"annotations" : annotations })
410+ # else:
411+ # Add annotations to layout to make text more readable when displaying heatmaps
412+
413+ # TODO: this doesn't work yet; resolve or scrap
414+ # cmap = get_cmap(PourbaixDiagramComponent.colorscale)
415+ # def get_text_color(x, y):
416+ # """
417+ # Set text color based on whether background at that point is dark or light.
418+ # """
419+ # energy = pourbaix_diagram.get_decomposition_energy(entry, pH=x, V=y)
420+ # c = [int(c * 255) for c in cmap(energy)[0:3]]
421+ # # borrowed from crystal_toolkit.components.structure
422+ # # TODO: move to utility function and ensure correct attribution for magic numbers
423+ # if 1 - (c[0] * 0.299 + c[1] * 0.587 + c[2] * 0.114) / 255 < 0.5:
424+ # font_color = "#000000"
425+ # else:
426+ # font_color = "#ffffff"
427+ # #print(energy, c, font_color)
428+ # return font_color
429+
430+ # def get_text_size(available_vertical_space):
431+ # """Set text size based on available vertical space."""
432+ # return min(max(6 * available_vertical_space, 12), 20)
433+
434+ # annotations = [
435+ # {
436+ # "align": "center",
437+ # "bgcolor": "white",
438+ # "font": {"color": "black", "size": get_text_size(height)},
439+ # "opacity": 1,
440+ # "showarrow": False,
441+ # "text": label,
442+ # "x": x,
443+ # "xanchor": "center",
444+ # "yanchor": "auto",
445+ # # "xshift": -10,
446+ # # "yshift": -10,
447+ # "xref": "x",
448+ # "y": y,
449+ # "yref": "y",
450+ # }
451+ # for (x, y), label, height in zip(xy_data, labels, domain_heights)
452+ # ]
453+ # layout.update({"annotations": annotations}) # shouldn't have annotation when heatmap_entry presents
452454
453455 # Get data for heatmap
454456 if heatmap_entry is not None :
@@ -562,7 +564,8 @@ def _sub_layouts(self) -> dict[str, Component]:
562564 [
563565 self .get_bool_input (
564566 "filter_solids" ,
565- state = self .default_state ,
567+ # state=self.default_state,
568+ default = self .default_state ["filter_solids" ],
566569 label = "Filter Solids" ,
567570 help_str = "Whether to filter solid phases by stability on the compositional phase diagram. "
568571 "The practical consequence of this is that highly oxidized or reduced phases that "
@@ -571,9 +574,16 @@ def _sub_layouts(self) -> dict[str, Component]:
571574 "overstabilized from DFT errors). Hence, including only the stable solid phases generally "
572575 "leads to the most accurate Pourbaix diagrams." ,
573576 ),
577+ html .Div (
578+ [
579+ html .Div (id = self .id ("element_specific_controls" )),
580+ ctl .Block (html .Div (id = self .id ("display-composition" ))),
581+ ]
582+ ),
574583 self .get_bool_input (
575584 "show_heatmap" , # kwarg_label
576- state = self .default_state ,
585+ # state=self.default_state,
586+ default = self .default_state ["show_heatmap" ],
577587 label = "Show Heatmap" ,
578588 help_str = "Hide or show a heatmap showing the decomposition energy for a specific "
579589 "entry in this system." ,
@@ -598,7 +608,6 @@ def _sub_layouts(self) -> dict[str, Component]:
598608 id = self .id ("heatmap_choice_container" ),
599609 style = {"width" : "250px" }, # better to assign a class for selection
600610 ),
601- html .Div (id = self .id ("element_specific_controls" )),
602611 ]
603612 )
604613
@@ -629,6 +638,8 @@ def update_heatmap_choices(entries, mat_detials):
629638 if not entries :
630639 raise PreventUpdate
631640
641+ print ("should be 4" )
642+
632643 options = []
633644 for entry in entries :
634645 if entry ["entry_id" ].startswith ("mp" ):
@@ -682,6 +693,94 @@ def update_heatmap_choices(entries, mat_detials):
682693 ),
683694 ]
684695
696+ @app .callback (
697+ Output (self .id ("element_specific_controls" ), "children" ),
698+ Input (self .id (), "data" ),
699+ # Input(self.get_kwarg_id("heatmap_choice"), "value"),
700+ # State(self.get_kwarg_id("show_heatmap"), "value"),
701+ prevent_initial_call = True ,
702+ )
703+ def update_element_specific_sliders (
704+ entries ,
705+ ): # , heatmap_choice, show_heatmap):
706+ """
707+ When pourbaix entries input, add concentration and composition options
708+ """
709+ if not entries :
710+ raise PreventUpdate
711+
712+ print ("should be 1" )
713+ elements = set ()
714+
715+ # kwargs = self.reconstruct_kwargs_from_state()
716+ # heatmap_choice = kwargs.get("heatmap_choice", None)
717+ # show_heatmap = kwargs.get("show_heatmap", False)
718+ # heatmap_entry = None
719+
720+ for entry in entries :
721+ if entry ["entry_id" ].startswith ("mp" ):
722+ composition = Composition (entry ["entry" ]["composition" ])
723+ elements .update (composition .elements )
724+ # if entry["entry_id"] == heatmap_choice:
725+ # heatmap_entry = entry
726+
727+ # exclude O and H
728+ elements = elements - ELEMENTS_HO
729+
730+ comp_defaults = {element : 1 / len (elements ) for element in elements }
731+
732+ comp_inputs = []
733+ conc_inputs = []
734+ for element in sorted (elements ):
735+ if len (elements ) > 1 :
736+ comp_input = html .Div (
737+ [
738+ self .get_slider_input (
739+ f"comp-{ element } " ,
740+ default = comp_defaults [element ],
741+ label = f"Composition of { element } " ,
742+ domain = [0 , 1 ],
743+ step = 0.01 ,
744+ )
745+ ]
746+ )
747+ comp_inputs .append (comp_input )
748+
749+ conc_input = html .Div (
750+ [
751+ self .get_numerical_input (
752+ f"conc-{ element } " ,
753+ default = 1e-6 ,
754+ min = MIN_CONCENTRATION ,
755+ max = MAX_CONCENTRATION ,
756+ label = f"Concentration of { element } ion" ,
757+ style = {"width" : "10rem" },
758+ )
759+ ]
760+ )
761+
762+ conc_inputs .append (conc_input )
763+
764+ comp_conc_controls = []
765+ # comp_conc_controls.append(
766+ # ctl.Block(html.Div(id=self.id("display-composition")))
767+ # )
768+ # if comp_inputs and (not show_heatmap) and (not heatmap_entry):
769+ # comp_conc_controls += comp_inputs
770+ comp_conc_controls += comp_inputs
771+
772+ ion_label = (
773+ "Set Ion Concentrations"
774+ if len (elements ) > 1
775+ else "Set Ion Concentration"
776+ )
777+ comp_conc_controls .append (ctl .Label (ion_label ))
778+
779+ comp_conc_controls += conc_inputs
780+
781+ return html .Div (comp_conc_controls )
782+
783+ """
685784 @app.callback(
686785 Output(self.id("element_specific_controls"), "children"),
687786 Output(self.id("ext-link"), "hidden"),
@@ -763,21 +862,26 @@ def update_element_specific_sliders(entries, heatmap_choice, show_heatmap):
763862 )
764863
765864 return html.Div(comp_conc_controls), False, external_link
865+ """
766866
767867 @app .callback (
768868 Output (self .id ("display-composition" ), "children" ),
769- Input (self .get_all_kwargs_id (), "value" ),
869+ Input (self .id ("element_specific_controls" ), "children" ),
870+ prevent_initial_call = True ,
871+ # Input(self.get_all_kwargs_id(), "value"),
770872 )
771- def update_displayed_composition (* args ):
873+ def update_displayed_composition (dependency ): # **kwargs ):
772874 kwargs = self .reconstruct_kwargs_from_state ()
773875
876+ print ("should be 2" )
877+
774878 comp_dict = {}
775879 for key , val in kwargs .items ():
776880 if "comp" in key : # keys are encoded like "comp-Ag"
777881 el = key .split ("-" )[1 ]
778882 comp_dict [el ] = val
779883 comp_dict = comp_dict or None
780-
884+ print ( comp_dict )
781885 if not comp_dict :
782886 return ""
783887
@@ -795,23 +899,30 @@ def update_displayed_composition(*args):
795899
796900 @cache .memoize (timeout = 5 * 60 )
797901 def get_pourbaix_diagram (pourbaix_entries , ** kwargs ):
902+ print ("yeee" )
903+ print (kwargs )
798904 return PourbaixDiagram (pourbaix_entries , ** kwargs )
799905
800906 @app .callback (
801907 Output (self .id ("graph" ), "figure" ),
802908 Input (self .id (), "data" ),
909+ Input (self .id ("display-composition" ), "children" ),
803910 Input (self .get_all_kwargs_id (), "value" ),
804911 )
805- def make_figure (pourbaix_entries , * args ) -> go .Figure :
912+ def make_figure (pourbaix_entries , dependency , kwargs ) -> go .Figure :
913+ # show_heatmap, heatmap_choice, filter_solids
806914 if pourbaix_entries is None :
807915 raise PreventUpdate
808916
917+ print ("should be 3" )
918+
809919 kwargs = self .reconstruct_kwargs_from_state ()
920+ print (kwargs )
810921
811922 pourbaix_entries = self .from_data (pourbaix_entries )
812923
813924 # Get heatmap id
814- if kwargs [ "show_heatmap" ] and kwargs .get ("heatmap_choice" ):
925+ if kwargs . get ( "show_heatmap" ) and kwargs .get ("heatmap_choice" ):
815926 # get Entry object based on the heatmap_choice, which is entry_id string
816927 heatmap_entry = next (
817928 entry
@@ -837,7 +948,8 @@ def make_figure(pourbaix_entries, *args) -> go.Figure:
837948 el = key .split ("-" )[1 ]
838949 comp_dict [el ] = val
839950 comp_dict = comp_dict or None
840-
951+ print ("yee2" )
952+ print (comp_dict )
841953 conc_dict = {}
842954 # e.g. kwargs contains {"conc-Ag": 1e-6, "conc-Fe": 1e-4},
843955 # essentially {slider_name: slider_value}
@@ -846,13 +958,15 @@ def make_figure(pourbaix_entries, *args) -> go.Figure:
846958 el = key .split ("-" )[1 ]
847959 conc_dict [el ] = val
848960 conc_dict = conc_dict or None
849-
961+ print ("yeee2" )
962+ print (conc_dict )
850963 pourbaix_diagram = get_pourbaix_diagram (
851964 pourbaix_entries ,
852965 comp_dict = comp_dict ,
853966 conc_dict = conc_dict ,
854967 filter_solids = kwargs ["filter_solids" ],
855968 )
969+
856970 self .logger .debug ( # noqa: PLE1205
857971 "Generated pourbaix diagram" ,
858972 len (pourbaix_entries ),
0 commit comments