From 89e11c6fffe8e3daab85132b3880d8b12b3ebc9c Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 22 Apr 2026 14:13:08 -0700 Subject: [PATCH 01/33] Update existing Pourbaix components, stylistic updates just for practice --- crystal_toolkit/apps/examples/pourbaix.py | 7 +- crystal_toolkit/components/pourbaix.py | 89 ++++++++--------------- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/crystal_toolkit/apps/examples/pourbaix.py b/crystal_toolkit/apps/examples/pourbaix.py index 56941932..549046e4 100644 --- a/crystal_toolkit/apps/examples/pourbaix.py +++ b/crystal_toolkit/apps/examples/pourbaix.py @@ -2,7 +2,8 @@ import dash from dash import html -from pymatgen.ext.matproj import MPRester +from mp_api.client import MPRester +import os import crystal_toolkit.components as ctc from crystal_toolkit.settings import SETTINGS @@ -15,8 +16,10 @@ # for more information. app.config["suppress_callback_exceptions"] = True +print("API_Key found:", os.environ.get("MP_API_KEY")) + # first, retrieve entries from Materials Project -with MPRester() as mpr: +with MPRester() as mpr: #os.environ.get("MP_API_KEY") pourbaix_entries = mpr.get_pourbaix_entries("Fe-Co") pourbaix_component = ctc.PourbaixDiagramComponent(default_data=pourbaix_entries) diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index 4dc2e251..4884ff13 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -471,56 +471,33 @@ def _sub_layouts(self) -> dict[str, Component]: ), html.Div( [ + ctl.add_label_help( + html.Div(), + label="Composition Control", + help="Set the relative composition of non-H/O elements.", + ), html.Div( - [ - html.H5( - "Composition Control", - style={ - "fontWeight": "bold", - "textAlign": "center", - "flex": "0 0 100%", - }, - ), - html.H5( - "Composition of", - id=self.id("composition-title"), - # style={"fontWeight": "bold"}, - ), - ], + id=self.id("composition-title"), + style={"marginBottom": "0.5rem"}, + ), + dcc.Input( + id=self.id("comp-text"), + className="input", + type="text", style={ - "line-height": PANEL_LINE_HEIGHT, - "display": "flex", - "flexWrap": "wrap", - "justifyContent": "center", + "textAlign": "center", + "width": "10rem", + "marginRight": "0.2rem", + "marginBottom": "0.2rem", + "height": "36px", + "fontSize": "14px", }, ), - PourbaixDiagramComponent.create_centered_object( - dcc.Input( - id=self.id("comp-text"), - className="input", - type="text", - style={ - "textAlign": "center", - "width": "10rem", - "marginRight": "0.2rem", - "marginBottom": "0.2rem", - "height": "36px", - "fontSize": "14px", - }, - ), - ), ctl.Block( - PourbaixDiagramComponent.create_centered_object( - html.Div( - id=self.id("display-composition"), - ) + html.Div( + id=self.id("display-composition"), ) ), - html.Hr( - style={ - "backgroundColor": "#C5C5C6", - } - ), dcc.Store(id=self.id("elements-store")), ], id=self.id("comp-panel"), @@ -543,7 +520,7 @@ def _sub_layouts(self) -> dict[str, Component]: ), ], style={ - "backgroundColor": "#F1F1F5", + "backgroundColor": "#FFFFFF", }, ), html.Br(), @@ -714,19 +691,17 @@ def update_element_specific_sliders( conc_inputs = [] for element in sorted(elements): - conc_input = PourbaixDiagramComponent.create_centered_object( - self.get_numerical_input( + conc_input = self.get_numerical_input( f"conc-{element}", default=1e-6, min=MIN_CONCENTRATION, max=MAX_CONCENTRATION, - label=f"concentration of {element} ion", + label=f"Concentration of {element} ion", style={ "width": "10rem", "fontSize": "14px", }, ) - ) conc_inputs.append(conc_input) @@ -739,18 +714,12 @@ def update_element_specific_sliders( ) comp_conc_controls.append( - html.H5( - ion_label, - style={"fontWeight": "bold", "textAlign": "center"}, + ctl.add_label_help( + html.Div(), + label=ion_label, + help=f"Equilibrium concentration beyond this threshold is considered unstable (meaningfully dissolved). Set the range between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} (M)", ), ) - comp_conc_controls.append( - PourbaixDiagramComponent.create_centered_object( - html.H6( - f"💡 Set the range between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} (M)" - ) - ) - ) comp_conc_controls += conc_inputs @@ -768,12 +737,12 @@ def update_element_specific_sliders( default_comp = ":".join(["1" for _ in elements]) # composition title - title = "💡 Composition of " + ":".join(elements) + title = "Composition of " + ":".join(elements) # update_string update_string = "Concentration update" if len(elements) > 1: - update_string = "Composition & concentration update" + update_string = "Update composition & concentrations" return ( html.Div( From e166e74f3b355c8737ef7c7758ea3098401ed265 Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 22 Apr 2026 14:13:29 -0700 Subject: [PATCH 02/33] Add reverse Pourbaix components for demo --- .../apps/examples/reverse_pourbaix_example.py | 52 ++ .../examples/reverse_pourbaix_heatmap.json | 707 ++++++++++++++++++ .../components/reverse_pourbaix.py | 283 +++++++ 3 files changed, 1042 insertions(+) create mode 100755 crystal_toolkit/apps/examples/reverse_pourbaix_example.py create mode 100755 crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json create mode 100755 crystal_toolkit/components/reverse_pourbaix.py diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py new file mode 100755 index 00000000..b170c484 --- /dev/null +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -0,0 +1,52 @@ +"""Example app for the Reverse Pourbaix Diagram Component. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import dash +from dash import html + +import crystal_toolkit.components as ctc +from crystal_toolkit.settings import SETTINGS +from crystal_toolkit.components.reverse_pourbaix import ReversePourbaixDiagramComponent + +app = dash.Dash(assets_folder=SETTINGS.ASSETS_PATH) +app.config["suppress_callback_exceptions"] = True + +# Load pre-computed heatmap data +DATA_PATH = Path(__file__).parent / "reverse_pourbaix_heatmap.json" + +print(f"Loading heatmap data from: {DATA_PATH}") + +with open(DATA_PATH) as f: + heatmap_data = json.load(f) + +print(f" Grid points: {len(heatmap_data['grid'])}") +print(f" pH range: {heatmap_data['ph_values'][0]} to {heatmap_data['ph_values'][-1]}") +print(f" V range: {heatmap_data['v_values'][-1]} to {heatmap_data['v_values'][0]}") + +# Create component +reverse_pourbaix_component = ReversePourbaixDiagramComponent( + default_data=heatmap_data +) + +# Layout +layout = html.Div( + [ + html.H1("Reverse Pourbaix Diagram"), + html.P( + "Number of thermodynamically stable materials at each pH/potential " + "combination (decomposition energy < 0.2 eV/atom)." + ), + reverse_pourbaix_component.layout(), + ], + style=dict(maxWidth="900px", margin="2em auto"), +) + +ctc.register_crystal_toolkit(app=app, layout=layout) + +if __name__ == "__main__": + app.run(debug=True, port=8050) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json new file mode 100755 index 00000000..a1bf8836 --- /dev/null +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json @@ -0,0 +1,707 @@ +{ + "ph_values": [ + 0.0, + 1.0, + 2.0, + 3.0, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0, + 11.0, + 12.0, + 13.0, + 14.0 + ], + "v_values": [ + 2.0, + 1.5, + 1.0, + 0.5, + 0.0, + -0.5, + -1.0, + -1.5, + -2.0 + ], + "grid": [ + { + "pH": 0.0, + "V": -2.0, + "count": 5620 + }, + { + "pH": 0.0, + "V": -1.5, + "count": 5136 + }, + { + "pH": 0.0, + "V": -1.0, + "count": 4664 + }, + { + "pH": 0.0, + "V": -0.5, + "count": 4946 + }, + { + "pH": 0.0, + "V": 0.0, + "count": 7209 + }, + { + "pH": 0.0, + "V": 0.5, + "count": 8887 + }, + { + "pH": 0.0, + "V": 1.0, + "count": 9633 + }, + { + "pH": 0.0, + "V": 1.5, + "count": 9465 + }, + { + "pH": 0.0, + "V": 2.0, + "count": 8651 + }, + { + "pH": 1.0, + "V": -2.0, + "count": 5653 + }, + { + "pH": 1.0, + "V": -1.5, + "count": 5277 + }, + { + "pH": 1.0, + "V": -1.0, + "count": 4927 + }, + { + "pH": 1.0, + "V": -0.5, + "count": 6009 + }, + { + "pH": 1.0, + "V": 0.0, + "count": 9155 + }, + { + "pH": 1.0, + "V": 0.5, + "count": 11351 + }, + { + "pH": 1.0, + "V": 1.0, + "count": 11963 + }, + { + "pH": 1.0, + "V": 1.5, + "count": 11736 + }, + { + "pH": 1.0, + "V": 2.0, + "count": 9952 + }, + { + "pH": 2.0, + "V": -2.0, + "count": 5713 + }, + { + "pH": 2.0, + "V": -1.5, + "count": 5497 + }, + { + "pH": 2.0, + "V": -1.0, + "count": 5344 + }, + { + "pH": 2.0, + "V": -0.5, + "count": 7313 + }, + { + "pH": 2.0, + "V": 0.0, + "count": 11461 + }, + { + "pH": 2.0, + "V": 0.5, + "count": 14374 + }, + { + "pH": 2.0, + "V": 1.0, + "count": 14728 + }, + { + "pH": 2.0, + "V": 1.5, + "count": 13291 + }, + { + "pH": 2.0, + "V": 2.0, + "count": 10378 + }, + { + "pH": 3.0, + "V": -2.0, + "count": 5787 + }, + { + "pH": 3.0, + "V": -1.5, + "count": 5800 + }, + { + "pH": 3.0, + "V": -1.0, + "count": 5940 + }, + { + "pH": 3.0, + "V": -0.5, + "count": 10015 + }, + { + "pH": 3.0, + "V": 0.0, + "count": 14999 + }, + { + "pH": 3.0, + "V": 0.5, + "count": 17533 + }, + { + "pH": 3.0, + "V": 1.0, + "count": 17440 + }, + { + "pH": 3.0, + "V": 1.5, + "count": 14434 + }, + { + "pH": 3.0, + "V": 2.0, + "count": 10538 + }, + { + "pH": 4.0, + "V": -2.0, + "count": 5852 + }, + { + "pH": 4.0, + "V": -1.5, + "count": 6127 + }, + { + "pH": 4.0, + "V": -1.0, + "count": 6780 + }, + { + "pH": 4.0, + "V": -0.5, + "count": 12897 + }, + { + "pH": 4.0, + "V": 0.0, + "count": 18848 + }, + { + "pH": 4.0, + "V": 0.5, + "count": 19301 + }, + { + "pH": 4.0, + "V": 1.0, + "count": 18614 + }, + { + "pH": 4.0, + "V": 1.5, + "count": 15282 + }, + { + "pH": 4.0, + "V": 2.0, + "count": 10773 + }, + { + "pH": 5.0, + "V": -2.0, + "count": 5906 + }, + { + "pH": 5.0, + "V": -1.5, + "count": 6611 + }, + { + "pH": 5.0, + "V": -1.0, + "count": 7764 + }, + { + "pH": 5.0, + "V": -0.5, + "count": 15315 + }, + { + "pH": 5.0, + "V": 0.0, + "count": 21106 + }, + { + "pH": 5.0, + "V": 0.5, + "count": 20598 + }, + { + "pH": 5.0, + "V": 1.0, + "count": 19246 + }, + { + "pH": 5.0, + "V": 1.5, + "count": 16042 + }, + { + "pH": 5.0, + "V": 2.0, + "count": 10958 + }, + { + "pH": 6.0, + "V": -2.0, + "count": 5985 + }, + { + "pH": 6.0, + "V": -1.5, + "count": 7194 + }, + { + "pH": 6.0, + "V": -1.0, + "count": 8524 + }, + { + "pH": 6.0, + "V": -0.5, + "count": 17402 + }, + { + "pH": 6.0, + "V": 0.0, + "count": 23256 + }, + { + "pH": 6.0, + "V": 0.5, + "count": 22272 + }, + { + "pH": 6.0, + "V": 1.0, + "count": 19627 + }, + { + "pH": 6.0, + "V": 1.5, + "count": 16855 + }, + { + "pH": 6.0, + "V": 2.0, + "count": 11107 + }, + { + "pH": 7.0, + "V": -2.0, + "count": 6108 + }, + { + "pH": 7.0, + "V": -1.5, + "count": 7773 + }, + { + "pH": 7.0, + "V": -1.0, + "count": 9018 + }, + { + "pH": 7.0, + "V": -0.5, + "count": 19190 + }, + { + "pH": 7.0, + "V": 0.0, + "count": 24198 + }, + { + "pH": 7.0, + "V": 0.5, + "count": 23848 + }, + { + "pH": 7.0, + "V": 1.0, + "count": 19737 + }, + { + "pH": 7.0, + "V": 1.5, + "count": 16200 + }, + { + "pH": 7.0, + "V": 2.0, + "count": 11205 + }, + { + "pH": 8.0, + "V": -2.0, + "count": 6245 + }, + { + "pH": 8.0, + "V": -1.5, + "count": 8406 + }, + { + "pH": 8.0, + "V": -1.0, + "count": 9765 + }, + { + "pH": 8.0, + "V": -0.5, + "count": 20350 + }, + { + "pH": 8.0, + "V": 0.0, + "count": 24214 + }, + { + "pH": 8.0, + "V": 0.5, + "count": 24044 + }, + { + "pH": 8.0, + "V": 1.0, + "count": 19461 + }, + { + "pH": 8.0, + "V": 1.5, + "count": 15441 + }, + { + "pH": 8.0, + "V": 2.0, + "count": 11106 + }, + { + "pH": 9.0, + "V": -2.0, + "count": 6374 + }, + { + "pH": 9.0, + "V": -1.5, + "count": 9069 + }, + { + "pH": 9.0, + "V": -1.0, + "count": 10817 + }, + { + "pH": 9.0, + "V": -0.5, + "count": 21450 + }, + { + "pH": 9.0, + "V": 0.0, + "count": 24167 + }, + { + "pH": 9.0, + "V": 0.5, + "count": 24018 + }, + { + "pH": 9.0, + "V": 1.0, + "count": 19226 + }, + { + "pH": 9.0, + "V": 1.5, + "count": 14887 + }, + { + "pH": 9.0, + "V": 2.0, + "count": 10934 + }, + { + "pH": 10.0, + "V": -2.0, + "count": 6344 + }, + { + "pH": 10.0, + "V": -1.5, + "count": 9418 + }, + { + "pH": 10.0, + "V": -1.0, + "count": 12967 + }, + { + "pH": 10.0, + "V": -0.5, + "count": 22788 + }, + { + "pH": 10.0, + "V": 0.0, + "count": 24236 + }, + { + "pH": 10.0, + "V": 0.5, + "count": 23772 + }, + { + "pH": 10.0, + "V": 1.0, + "count": 18824 + }, + { + "pH": 10.0, + "V": 1.5, + "count": 14025 + }, + { + "pH": 10.0, + "V": 2.0, + "count": 10692 + }, + { + "pH": 11.0, + "V": -2.0, + "count": 6379 + }, + { + "pH": 11.0, + "V": -1.5, + "count": 10004 + }, + { + "pH": 11.0, + "V": -1.0, + "count": 14115 + }, + { + "pH": 11.0, + "V": -0.5, + "count": 24216 + }, + { + "pH": 11.0, + "V": 0.0, + "count": 23808 + }, + { + "pH": 11.0, + "V": 0.5, + "count": 23341 + }, + { + "pH": 11.0, + "V": 1.0, + "count": 18346 + }, + { + "pH": 11.0, + "V": 1.5, + "count": 13045 + }, + { + "pH": 11.0, + "V": 2.0, + "count": 10221 + }, + { + "pH": 12.0, + "V": -2.0, + "count": 6623 + }, + { + "pH": 12.0, + "V": -1.5, + "count": 10921 + }, + { + "pH": 12.0, + "V": -1.0, + "count": 15518 + }, + { + "pH": 12.0, + "V": -0.5, + "count": 23426 + }, + { + "pH": 12.0, + "V": 0.0, + "count": 23347 + }, + { + "pH": 12.0, + "V": 0.5, + "count": 22856 + }, + { + "pH": 12.0, + "V": 1.0, + "count": 17245 + }, + { + "pH": 12.0, + "V": 1.5, + "count": 12146 + }, + { + "pH": 12.0, + "V": 2.0, + "count": 9489 + }, + { + "pH": 13.0, + "V": -2.0, + "count": 7041 + }, + { + "pH": 13.0, + "V": -1.5, + "count": 11472 + }, + { + "pH": 13.0, + "V": -1.0, + "count": 16400 + }, + { + "pH": 13.0, + "V": -0.5, + "count": 21638 + }, + { + "pH": 13.0, + "V": 0.0, + "count": 22250 + }, + { + "pH": 13.0, + "V": 0.5, + "count": 21634 + }, + { + "pH": 13.0, + "V": 1.0, + "count": 15533 + }, + { + "pH": 13.0, + "V": 1.5, + "count": 11052 + }, + { + "pH": 13.0, + "V": 2.0, + "count": 8678 + }, + { + "pH": 14.0, + "V": -2.0, + "count": 7769 + }, + { + "pH": 14.0, + "V": -1.5, + "count": 11383 + }, + { + "pH": 14.0, + "V": -1.0, + "count": 16389 + }, + { + "pH": 14.0, + "V": -0.5, + "count": 19883 + }, + { + "pH": 14.0, + "V": 0.0, + "count": 20626 + }, + { + "pH": 14.0, + "V": 0.5, + "count": 20014 + }, + { + "pH": 14.0, + "V": 1.0, + "count": 13583 + }, + { + "pH": 14.0, + "V": 1.5, + "count": 10060 + }, + { + "pH": 14.0, + "V": 2.0, + "count": 8005 + } + ] +} \ No newline at end of file diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py new file mode 100755 index 00000000..3239e3de --- /dev/null +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -0,0 +1,283 @@ +"""Reverse Pourbaix Diagram Component. + +Displays a heatmap of the number of thermodynamically stable materials +across pH and potential (V_SHE) space, based on pre-computed Pourbaix +stability data from the Materials Project database. + +This is the "reverse" of the standard Pourbaix diagram: instead of showing +stability domains for a single material, it shows how many materials are +stable at each electrochemical condition. + +See: Karlsson et al., Electrochimica Acta 549 (2026) 148053 +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +import numpy as np +import plotly.graph_objects as go +from dash import dcc, html +from dash.dependencies import Component, Input, Output +from dash.exceptions import PreventUpdate +from frozendict import frozendict + +from pymatgen.analysis.pourbaix_diagram import PREFAC + +from crystal_toolkit.core.mpcomponent import MPComponent + +logger = logging.getLogger(__name__) + +__author__ = "Leo Karlsson" + +# Grid and display constants +HEIGHT = 550 +WIDTH = 700 +MIN_PH = 0 +MAX_PH = 14 +MIN_V = -2 +MAX_V = 2 + +class ReversePourbaixDiagramComponent(MPComponent): + """Component for displaying a reverse Pourbaix diagram. + + Shows a heatmap of the number of stable materials at each pH/V + combination. + """ + + default_state = frozendict( + show_water_lines=True, + ) + + default_plot_style = frozendict( + xaxis={ + "title": "pH", + "anchor": "y", + "mirror": "ticks", + "showgrid": False, + "showline": True, + "side": "bottom", + "tickfont": {"size": 16.0}, + "ticks": "inside", + "title": {"font": {"color": "#000000", "size": 24.0}, "text": "pH"}, + "type": "linear", + "zeroline": False, + "range": [MIN_PH, MAX_PH], + }, + yaxis={ + "title": "Potential (V vs. SHE)", + "anchor": "x", + "mirror": "ticks", + "range": [MIN_V, MAX_V], + "showgrid": False, + "showline": True, + "side": "left", + "tickfont": {"size": 16.0}, + "ticks": "inside", + "title": {"font": {"color": "#000000", "size": 24.0}, "text": "Potential (V vs. SHE)"}, + "type": "linear", + "zeroline": False, + }, + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + height=HEIGHT, + width=WIDTH, + hovermode="closest", + showlegend=False, + margin=dict(l=80, b=70, t=10, r=20), + ) + + empty_plot_style = frozendict( + xaxis={"visible": False}, + yaxis={"visible": False}, + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + ) + + @staticmethod + def get_heatmap_figure( + heatmap_data: dict, + show_water_lines: bool = True, + selected_ph: float | None = None, + selected_v: float | None = None, + ) -> go.Figure: + """Generate a Plotly heatmap figure from pre-computed data. + + Args: + heatmap_data: dict with keys 'ph_values', 'v_values', 'grid' + grid is a list of {'pH': float, 'V': float, 'count': int} + show_water_lines: if True, show HER/OER stability lines + selected_ph: pH value of selected cell (for highlight) + selected_v: V value of selected cell (for highlight) + + Returns: + go.Figure with the heatmap + """ + ph_values = heatmap_data["ph_values"] + v_values = heatmap_data["v_values"] + grid = heatmap_data["grid"] + + # Build the Z matrix + # Create a lookup for fast access + lookup = {} + for point in grid: + lookup[(point["pH"], point["V"])] = point["count"] + + z_matrix = [] + for v in v_values: + row = [] + for ph in ph_values: + row.append(lookup.get((ph, v), 0)) + z_matrix.append(row) + + data = [] + + # Heatmap trace + heatmap_trace = go.Heatmap( + z=z_matrix, + x=ph_values, + y=v_values, + colorscale="Viridis", + colorbar={"title": "Number of
Materials"}, + hovertemplate=( + "pH: %{x}
" + "V: %{y} VSHE
" + "Stable materials: %{z}" + "" + ), + ) + data.append(heatmap_trace) + + # Water stability lines + if show_water_lines: + ph_range = [MIN_PH, MAX_PH] + + # Hydrogen evolution line + data.append( + go.Scatter( + x=ph_range, + y=[-ph_range[0] * PREFAC, -ph_range[1] * PREFAC], + mode="lines", + line={"color": "white", "dash": "dash", "width": 2}, + name="H₂/H₂O", + hoverinfo="skip", + showlegend=False, + ) + ) + + # Oxygen evolution line + data.append( + go.Scatter( + x=ph_range, + y=[ + -ph_range[0] * PREFAC + 1.23, + -ph_range[1] * PREFAC + 1.23, + ], + mode="lines", + line={"color": "white", "dash": "dash", "width": 2}, + name="O₂/H₂O", + hoverinfo="skip", + showlegend=False, + ) + ) + + layout = {**ReversePourbaixDiagramComponent.default_plot_style} + + # Add selection rectangle if a cell is selected + if selected_ph is not None and selected_v is not None: + # Calculate cell boundaries + ph_step = ph_values[1] - ph_values[0] if len(ph_values) > 1 else 1 + v_step = abs(v_values[0] - v_values[1]) if len(v_values) > 1 else 0.5 + + layout["shapes"] = [ + { + "type": "rect", + "x0": selected_ph - ph_step / 2, + "x1": selected_ph + ph_step / 2, + "y0": selected_v - v_step / 2, + "y1": selected_v + v_step / 2, + "line": {"color": "white", "width": 3}, + "fillcolor": "rgba(0,0,0,0)", + } + ] + + return go.Figure(data=data, layout=layout) + + @property + def _sub_layouts(self) -> dict[str, Component]: + graph = html.Div( + [ + dcc.Graph( + figure=go.Figure( + layout={**ReversePourbaixDiagramComponent.empty_plot_style} + ), + responsive=True, + config={"displayModeBar": False, "displaylogo": False}, + ), + ], + style={"minHeight": "500px"}, + id=self.id("graph-panel"), + ) + + info = html.Div( + id=self.id("click-info"), + style={ + "padding": "10px", + "textAlign": "center", + "color": "#666", + }, + children="Click on the heatmap to explore stable materials at a specific condition.", + ) + + options = html.Div( + [ + self.get_bool_input( + "show_water_lines", + default=self.default_state["show_water_lines"], + label="Show Water Stability Lines", + help_str="Show the hydrogen and oxygen evolution reaction lines.", + ), + ] + ) + + return {"graph": graph, "info": info, "options": options} + + def layout(self) -> html.Div: + """Return the full component layout.""" + return html.Div( + children=[ + self._sub_layouts["options"], + self._sub_layouts["graph"], + self._sub_layouts["info"], + ] + ) + + def generate_callbacks(self, app, cache) -> None: + """Register Dash callbacks for interactivity.""" + + @app.callback( + Output(self.id("graph-panel"), "children"), + Input(self.id(), "data"), + Input(self.get_kwarg_id("show_water_lines"), "value"), + ) + def update_figure(heatmap_json, show_water_lines): + if not heatmap_json: + raise PreventUpdate + + heatmap_data = self.from_data(heatmap_json) + + kwargs = self.reconstruct_kwargs_from_state() + show_water_lines = kwargs.get("show_water_lines", True) + + figure = self.get_heatmap_figure( + heatmap_data, + show_water_lines=show_water_lines, + ) + + return dcc.Graph( + figure=figure, + responsive=True, + config={"displayModeBar": False, "displaylogo": False}, + ) From 14d055df8bdad14b9e75e01c95f0403eeb64d50a Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 24 Apr 2026 11:53:21 -0700 Subject: [PATCH 03/33] small changes to reverse_pourbaix, added reverse pourbiax to init --- crystal_toolkit/components/__init__.py | 1 + crystal_toolkit/components/reverse_pourbaix.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crystal_toolkit/components/__init__.py b/crystal_toolkit/components/__init__.py index e4137033..124772ba 100644 --- a/crystal_toolkit/components/__init__.py +++ b/crystal_toolkit/components/__init__.py @@ -18,6 +18,7 @@ PhononBandstructureAndDosPanelComponent, ) from crystal_toolkit.components.pourbaix import PourbaixDiagramComponent +from crystal_toolkit.components.reverse_pourbaix import ReversePourbaixDiagramComponent from crystal_toolkit.components.search import SearchComponent from crystal_toolkit.components.structure import StructureMoleculeComponent diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 3239e3de..3c14006f 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -237,7 +237,7 @@ def _sub_layouts(self) -> dict[str, Component]: "show_water_lines", default=self.default_state["show_water_lines"], label="Show Water Stability Lines", - help_str="Show the hydrogen and oxygen evolution reaction lines.", + help_str="Show the hydrogen and oxygen evolution reaction lines. Potential scale is SHE.", ), ] ) From b644096bae2aab7c857e6a22ad29efc1814a8640 Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 24 Apr 2026 12:06:41 -0700 Subject: [PATCH 04/33] addition of dummy stability cutoff slider --- crystal_toolkit/components/reverse_pourbaix.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 3c14006f..161bb310 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -239,6 +239,16 @@ def _sub_layouts(self) -> dict[str, Component]: label="Show Water Stability Lines", help_str="Show the hydrogen and oxygen evolution reaction lines. Potential scale is SHE.", ), + self.get_slider_input( + kwarg_label="stability_cutoff", + default=0.1, + domain=[0.1, 0.5], + step=0.1, + label = "Stability Cutoff (eV)", + help_str=""" + Heatmap will display the count of materials below this stability cutoff. + The stability cutoff is based on the energy above the Pourbaix hull (decomposition energy, Gpbx)""", + ), ] ) From 593a3c3665d6249b829f278ca9f958d30ad89d85 Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 27 Apr 2026 13:48:19 -0700 Subject: [PATCH 05/33] reverse pourbaix with eV cutoff now, heatmap updates at selected cutoff --- .../components/reverse_pourbaix.py | 112 ++++++++++++------ 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 161bb310..0aa08fbb 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -13,11 +13,9 @@ from __future__ import annotations -import json import logging -from pathlib import Path +from typing import Any -import numpy as np import plotly.graph_objects as go from dash import dcc, html from dash.dependencies import Component, Input, Output @@ -40,15 +38,25 @@ MIN_V = -2 MAX_V = 2 +# Stability cutoff (eV/atom) — the recommended practical value is 0.2, +# matching the metastability threshold used in Karlsson et al. and the +# upstream MP/Sun/Aykol references. +DEFAULT_CUTOFF = 0.2 +CUTOFF_RANGE = [0.1, 0.5] +CUTOFF_STEP = 0.1 + + class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. Shows a heatmap of the number of stable materials at each pH/V - combination. + combination, where stability is defined by a user-tunable + decomposition energy cutoff (eV/atom). """ default_state = frozendict( show_water_lines=True, + stability_cutoff=DEFAULT_CUTOFF, ) default_plot_style = frozendict( @@ -96,9 +104,18 @@ class ReversePourbaixDiagramComponent(MPComponent): plot_bgcolor="rgba(0,0,0,0)", ) + @staticmethod + def _format_cutoff_key(cutoff: float) -> str: + """Format a cutoff float to match the JSON key convention. + + JSON keys are stored as e.g. "0.1", "0.2" — i.e. one decimal. + """ + return f"{cutoff:.1f}" + @staticmethod def get_heatmap_figure( - heatmap_data: dict, + heatmap_data: dict[str, Any], + stability_cutoff: float = DEFAULT_CUTOFF, show_water_lines: bool = True, selected_ph: float | None = None, selected_v: float | None = None, @@ -106,33 +123,38 @@ def get_heatmap_figure( """Generate a Plotly heatmap figure from pre-computed data. Args: - heatmap_data: dict with keys 'ph_values', 'v_values', 'grid' - grid is a list of {'pH': float, 'V': float, 'count': int} - show_water_lines: if True, show HER/OER stability lines - selected_ph: pH value of selected cell (for highlight) - selected_v: V value of selected cell (for highlight) + heatmap_data: dict with keys 'ph_values', 'v_values', 'cutoffs', 'grid'. + Each grid entry is {'pH': float, 'V': float, + 'counts': {cutoff_str: int, ...}}. + stability_cutoff: decomposition-energy cutoff (eV/atom) selected + by the user. Must match one of the precomputed cutoffs in + heatmap_data['cutoffs']. + show_water_lines: if True, show HER/OER stability lines. + selected_ph: pH value of selected cell (for highlight). + selected_v: V value of selected cell (for highlight). Returns: - go.Figure with the heatmap + go.Figure with the heatmap. """ ph_values = heatmap_data["ph_values"] v_values = heatmap_data["v_values"] grid = heatmap_data["grid"] - # Build the Z matrix - # Create a lookup for fast access - lookup = {} - for point in grid: - lookup[(point["pH"], point["V"])] = point["count"] + cutoff_key = ReversePourbaixDiagramComponent._format_cutoff_key(stability_cutoff) - z_matrix = [] - for v in v_values: - row = [] - for ph in ph_values: - row.append(lookup.get((ph, v), 0)) - z_matrix.append(row) + # Build a (pH, V) -> count lookup for the requested cutoff. + # The slider's domain/step is constrained to the precomputed cutoffs, + # so a missing key here indicates a developer error (slider config and + # JSON have drifted out of sync) and should raise rather than be hidden. + lookup: dict[tuple[float, float], int] = { + (point["pH"], point["V"]): point["counts"][cutoff_key] for point in grid + } - data = [] + z_matrix = [ + [lookup.get((ph, v), 0) for ph in ph_values] for v in v_values + ] + + data: list[go.BaseTraceType] = [] # Heatmap trace heatmap_trace = go.Heatmap( @@ -187,7 +209,6 @@ def get_heatmap_figure( # Add selection rectangle if a cell is selected if selected_ph is not None and selected_v is not None: - # Calculate cell boundaries ph_step = ph_values[1] - ph_values[0] if len(ph_values) > 1 else 1 v_step = abs(v_values[0] - v_values[1]) if len(v_values) > 1 else 0.5 @@ -237,17 +258,24 @@ def _sub_layouts(self) -> dict[str, Component]: "show_water_lines", default=self.default_state["show_water_lines"], label="Show Water Stability Lines", - help_str="Show the hydrogen and oxygen evolution reaction lines. Potential scale is SHE.", + help_str=( + "Show the hydrogen and oxygen evolution reaction lines. " + "Potential scale is SHE." + ), ), self.get_slider_input( kwarg_label="stability_cutoff", - default=0.1, - domain=[0.1, 0.5], - step=0.1, - label = "Stability Cutoff (eV)", - help_str=""" - Heatmap will display the count of materials below this stability cutoff. - The stability cutoff is based on the energy above the Pourbaix hull (decomposition energy, Gpbx)""", + default=self.default_state["stability_cutoff"], + domain=CUTOFF_RANGE, + step=CUTOFF_STEP, + label="Stability Cutoff (eV/atom)", + help_str=( + "Materials with a decomposition energy (G_pbx, distance " + "from the Pourbaix hull) below this cutoff are counted as " + "stable. The recommended value is 0.2 eV/atom, the " + "practical metastability threshold used in Karlsson et al. " + "Higher cutoffs include progressively more metastable phases." + ), ), ] ) @@ -271,23 +299,33 @@ def generate_callbacks(self, app, cache) -> None: Output(self.id("graph-panel"), "children"), Input(self.id(), "data"), Input(self.get_kwarg_id("show_water_lines"), "value"), + Input(self.get_kwarg_id("stability_cutoff"), "value"), ) - def update_figure(heatmap_json, show_water_lines): + def update_figure(heatmap_json, show_water_lines, stability_cutoff): if not heatmap_json: raise PreventUpdate heatmap_data = self.from_data(heatmap_json) - kwargs = self.reconstruct_kwargs_from_state() - show_water_lines = kwargs.get("show_water_lines", True) + # The kwarg inputs come back wrapped (typically as a list) by + # MPComponent's input machinery; unwrap defensively. + if isinstance(show_water_lines, list): + show_water_lines = show_water_lines[0] if show_water_lines else True + if isinstance(stability_cutoff, list): + stability_cutoff = ( + stability_cutoff[0] if stability_cutoff else DEFAULT_CUTOFF + ) + if stability_cutoff is None: + stability_cutoff = DEFAULT_CUTOFF figure = self.get_heatmap_figure( heatmap_data, - show_water_lines=show_water_lines, + stability_cutoff=float(stability_cutoff), + show_water_lines=bool(show_water_lines), ) return dcc.Graph( figure=figure, responsive=True, config={"displayModeBar": False, "displaylogo": False}, - ) + ) \ No newline at end of file From 8459956bda2e94148f9bea72cf59f0c7c19aca56 Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 27 Apr 2026 14:23:32 -0700 Subject: [PATCH 06/33] working demo of full list of mp_ids in each heatmap region displayed upon click --- .../components/reverse_pourbaix.py | 143 ++++++++++++------ 1 file changed, 99 insertions(+), 44 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 0aa08fbb..ecde697f 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -14,11 +14,14 @@ from __future__ import annotations import logging +from pathlib import Path from typing import Any +import pandas as pd import plotly.graph_objects as go +import pyarrow.parquet as pq from dash import dcc, html -from dash.dependencies import Component, Input, Output +from dash.dependencies import Component, Input, Output, State from dash.exceptions import PreventUpdate from frozendict import frozendict @@ -51,7 +54,8 @@ class ReversePourbaixDiagramComponent(MPComponent): Shows a heatmap of the number of stable materials at each pH/V combination, where stability is defined by a user-tunable - decomposition energy cutoff (eV/atom). + decomposition energy cutoff (eV/atom). Clicking on a cell exposes + the list of mp_ids stable at that condition for downstream use. """ default_state = frozendict( @@ -104,6 +108,26 @@ class ReversePourbaixDiagramComponent(MPComponent): plot_bgcolor="rgba(0,0,0,0)", ) + def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): + """ + Args: + parquet_path: path to the precomputed (pH, V, mp_id, decomposition_energy) + parquet file. Loaded once at component construction. If None, the + click-to-list functionality is disabled but the heatmap still works. + """ + super().__init__(*args, **kwargs) + self._stability_df: pd.DataFrame | None = None + if parquet_path is not None: + logger.info("Loading reverse-Pourbaix stability data from %s", parquet_path) + df = pq.read_table(parquet_path).to_pandas() + # Index by (pH, V) for fast cell-click lookups. + self._stability_df = df.set_index(["pH", "V"]).sort_index() + logger.info( + "Loaded %d stability rows across %d cells", + len(df), + self._stability_df.index.nunique(), + ) + @staticmethod def _format_cutoff_key(cutoff: float) -> str: """Format a cutoff float to match the JSON key convention. @@ -112,6 +136,24 @@ def _format_cutoff_key(cutoff: float) -> str: """ return f"{cutoff:.1f}" + def get_stable_mp_ids(self, ph: float, v: float, cutoff: float) -> list[str]: + """Return mp_ids stable at (pH, V) below the given decomposition-energy cutoff. + + Returns an empty list if the parquet data is not loaded. + """ + if self._stability_df is None: + return [] + # Round to handle float-equality edge cases in the index lookup. + ph_key = int(round(ph)) + v_key = round(v * 2) / 2 # snap to 0.5 grid + try: + cell = self._stability_df.loc[(ph_key, v_key)] + except KeyError: + logger.warning("No stability data for (pH=%s, V=%s)", ph_key, v_key) + return [] + stable = cell[cell["decomposition_energy"] <= cutoff] + return stable["mp_id"].tolist() + @staticmethod def get_heatmap_figure( heatmap_data: dict[str, Any], @@ -120,32 +162,13 @@ def get_heatmap_figure( selected_ph: float | None = None, selected_v: float | None = None, ) -> go.Figure: - """Generate a Plotly heatmap figure from pre-computed data. - - Args: - heatmap_data: dict with keys 'ph_values', 'v_values', 'cutoffs', 'grid'. - Each grid entry is {'pH': float, 'V': float, - 'counts': {cutoff_str: int, ...}}. - stability_cutoff: decomposition-energy cutoff (eV/atom) selected - by the user. Must match one of the precomputed cutoffs in - heatmap_data['cutoffs']. - show_water_lines: if True, show HER/OER stability lines. - selected_ph: pH value of selected cell (for highlight). - selected_v: V value of selected cell (for highlight). - - Returns: - go.Figure with the heatmap. - """ + """Generate a Plotly heatmap figure from pre-computed data.""" ph_values = heatmap_data["ph_values"] v_values = heatmap_data["v_values"] grid = heatmap_data["grid"] cutoff_key = ReversePourbaixDiagramComponent._format_cutoff_key(stability_cutoff) - # Build a (pH, V) -> count lookup for the requested cutoff. - # The slider's domain/step is constrained to the precomputed cutoffs, - # so a missing key here indicates a developer error (slider config and - # JSON have drifted out of sync) and should raise rather than be hidden. lookup: dict[tuple[float, float], int] = { (point["pH"], point["V"]): point["counts"][cutoff_key] for point in grid } @@ -156,7 +179,6 @@ def get_heatmap_figure( data: list[go.BaseTraceType] = [] - # Heatmap trace heatmap_trace = go.Heatmap( z=z_matrix, x=ph_values, @@ -172,11 +194,8 @@ def get_heatmap_figure( ) data.append(heatmap_trace) - # Water stability lines if show_water_lines: ph_range = [MIN_PH, MAX_PH] - - # Hydrogen evolution line data.append( go.Scatter( x=ph_range, @@ -188,15 +207,10 @@ def get_heatmap_figure( showlegend=False, ) ) - - # Oxygen evolution line data.append( go.Scatter( x=ph_range, - y=[ - -ph_range[0] * PREFAC + 1.23, - -ph_range[1] * PREFAC + 1.23, - ], + y=[-ph_range[0] * PREFAC + 1.23, -ph_range[1] * PREFAC + 1.23], mode="lines", line={"color": "white", "dash": "dash", "width": 2}, name="O₂/H₂O", @@ -207,11 +221,9 @@ def get_heatmap_figure( layout = {**ReversePourbaixDiagramComponent.default_plot_style} - # Add selection rectangle if a cell is selected if selected_ph is not None and selected_v is not None: ph_step = ph_values[1] - ph_values[0] if len(ph_values) > 1 else 1 v_step = abs(v_values[0] - v_values[1]) if len(v_values) > 1 else 0.5 - layout["shapes"] = [ { "type": "rect", @@ -231,6 +243,7 @@ def _sub_layouts(self) -> dict[str, Component]: graph = html.Div( [ dcc.Graph( + id=self.id("heatmap"), figure=go.Figure( layout={**ReversePourbaixDiagramComponent.empty_plot_style} ), @@ -242,14 +255,14 @@ def _sub_layouts(self) -> dict[str, Component]: id=self.id("graph-panel"), ) + # Holds the list of mp_ids stable at the most recently clicked cell. + # Downstream callbacks (filtering, table rendering, etc.) can read this. + mp_id_store = dcc.Store(id=self.id("stable-mp-ids"), data=[]) + info = html.Div( id=self.id("click-info"), - style={ - "padding": "10px", - "textAlign": "center", - "color": "#666", - }, - children="Click on the heatmap to explore stable materials at a specific condition.", + style={"padding": "10px", "color": "#444"}, + children="Click on the heatmap to see the list of stable materials at that condition.", ) options = html.Div( @@ -280,7 +293,7 @@ def _sub_layouts(self) -> dict[str, Component]: ] ) - return {"graph": graph, "info": info, "options": options} + return {"graph": graph, "info": info, "options": options, "store": mp_id_store} def layout(self) -> html.Div: """Return the full component layout.""" @@ -288,6 +301,7 @@ def layout(self) -> html.Div: children=[ self._sub_layouts["options"], self._sub_layouts["graph"], + self._sub_layouts["store"], self._sub_layouts["info"], ] ) @@ -307,8 +321,6 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): heatmap_data = self.from_data(heatmap_json) - # The kwarg inputs come back wrapped (typically as a list) by - # MPComponent's input machinery; unwrap defensively. if isinstance(show_water_lines, list): show_water_lines = show_water_lines[0] if show_water_lines else True if isinstance(stability_cutoff, list): @@ -325,7 +337,50 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): ) return dcc.Graph( + id=self.id("heatmap"), # keep stable id so clickData callback can find it figure=figure, responsive=True, config={"displayModeBar": False, "displaylogo": False}, - ) \ No newline at end of file + ) + + @app.callback( + Output(self.id("stable-mp-ids"), "data"), + Output(self.id("click-info"), "children"), + Input(self.id("heatmap"), "clickData"), + State(self.get_kwarg_id("stability_cutoff"), "value"), + ) + def on_cell_click(click_data, stability_cutoff): + if not click_data: + raise PreventUpdate + + point = click_data["points"][0] + ph = point["x"] + v = point["y"] + + if isinstance(stability_cutoff, list): + stability_cutoff = ( + stability_cutoff[0] if stability_cutoff else DEFAULT_CUTOFF + ) + if stability_cutoff is None: + stability_cutoff = DEFAULT_CUTOFF + cutoff = float(stability_cutoff) + + mp_ids = self.get_stable_mp_ids(ph, v, cutoff) + + preview = ", ".join(mp_ids[:20]) + more = f" ... (+{len(mp_ids) - 20} more)" if len(mp_ids) > 20 else "" + info = html.Div( + [ + html.Div( + f"pH = {ph}, V = {v} V (SHE), cutoff ≤ {cutoff} eV/atom", + style={"fontWeight": "bold"}, + ), + html.Div(f"{len(mp_ids)} stable materials"), + html.Div( + preview + more, + style={"fontFamily": "monospace", "marginTop": "0.5em"}, + ), + ] + ) + + return mp_ids, info \ No newline at end of file From 5adf13f0419948e6e9c9318f8378b021a630fcca Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 29 Apr 2026 10:40:14 -0700 Subject: [PATCH 07/33] stylistic changes to reverse pourbaix app, added comment to pourbaix app about filter_solids --- crystal_toolkit/components/pourbaix.py | 3 +++ crystal_toolkit/components/reverse_pourbaix.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index 4884ff13..ce13dfa1 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -598,6 +598,9 @@ def update_heatmap_choices(entries, mat_detials, filter_solids): entry for entry in entries_obj if entry.phase_type == "Solid" ] + #NOTE: filter_solids bool affects both heatmap choices and decomposition energy. + # Some user may want to find the decomposition eneryg of an mp-id (which is skipped in the choices) while creating the actual pbx with filter_solids = True. + # Pymatgen allows for this functionality, but currently this pourbaix UI does not allow for it. if filter_solids: # O is 2.46 b/c pbx entry finds energies referenced to H2O entries_HO = [ComputedEntry("H", 0), ComputedEntry("O", 2.46)] diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index ecde697f..ca87bf2c 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -262,7 +262,7 @@ def _sub_layouts(self) -> dict[str, Component]: info = html.Div( id=self.id("click-info"), style={"padding": "10px", "color": "#444"}, - children="Click on the heatmap to see the list of stable materials at that condition.", + children="Click on the heatmap to see the list of stable materials at those conditions.", ) options = html.Div( @@ -372,8 +372,17 @@ def on_cell_click(click_data, stability_cutoff): info = html.Div( [ html.Div( - f"pH = {ph}, V = {v} V (SHE), cutoff ≤ {cutoff} eV/atom", - style={"fontWeight": "bold"}, + [ + html.B("Selected grid point: "), + "pH = ", + html.B(f"{ph}"), + ", V", + html.Sub("SHE"), + " = ", + html.B(f"{v} V"), + ", cutoff ≤ ", + html.B(f"{cutoff} eV/atom"), + ], ), html.Div(f"{len(mp_ids)} stable materials"), html.Div( From b3467374d7299fa895a1a66a53899a2ffb7dbd4f Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 30 Apr 2026 15:06:07 -0700 Subject: [PATCH 08/33] small stylistic changes to reverse pourbaix app --- .../components/reverse_pourbaix.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index ca87bf2c..222a8ac4 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -251,7 +251,7 @@ def _sub_layouts(self) -> dict[str, Component]: config={"displayModeBar": False, "displaylogo": False}, ), ], - style={"minHeight": "500px"}, + #style={"minHeight": "500px"}, id=self.id("graph-panel"), ) @@ -259,10 +259,21 @@ def _sub_layouts(self) -> dict[str, Component]: # Downstream callbacks (filtering, table rendering, etc.) can read this. mp_id_store = dcc.Store(id=self.id("stable-mp-ids"), data=[]) + # Selection panel — mirrors the panel structure the app uses for + # Options so they render identically when stacked. The click callback + # writes to the inner content div (id "click-info"); the outer panel + # chrome is static. info = html.Div( - id=self.id("click-info"), - style={"padding": "10px", "color": "#444"}, - children="Click on the heatmap to see the list of stable materials at those conditions.", + [ + html.Div("Selected conditions", className="panel-heading"), + html.Div( + "Click on the heatmap to see the list of stable materials " + "at those conditions.", + id=self.id("click-info"), + className="panel-block is-block", + ), + ], + className="panel", ) options = html.Div( @@ -367,29 +378,23 @@ def on_cell_click(click_data, stability_cutoff): mp_ids = self.get_stable_mp_ids(ph, v, cutoff) - preview = ", ".join(mp_ids[:20]) - more = f" ... (+{len(mp_ids) - 20} more)" if len(mp_ids) > 20 else "" info = html.Div( [ - html.Div( - [ - html.B("Selected grid point: "), - "pH = ", - html.B(f"{ph}"), - ", V", - html.Sub("SHE"), - " = ", - html.B(f"{v} V"), - ", cutoff ≤ ", - html.B(f"{cutoff} eV/atom"), - ], - ), + html.Div([ + "pH = ", + html.B(f"{ph}"), + ", V", + html.Sub("SHE"), + " = ", + html.B(f"{v} V"), + ]), + html.Div([ + "cutoff ≤ ", + html.B(f"{cutoff} eV/atom"), + ]), html.Div(f"{len(mp_ids)} stable materials"), - html.Div( - preview + more, - style={"fontFamily": "monospace", "marginTop": "0.5em"}, - ), ] ) + return mp_ids, info \ No newline at end of file From 47b1ad67cf2e2cc5ce37d1fa70934c81921d73ac Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 1 May 2026 11:37:17 -0700 Subject: [PATCH 09/33] small refactor of code, functionality looks the same --- .../components/reverse_pourbaix.py | 77 +++++-------------- 1 file changed, 19 insertions(+), 58 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 222a8ac4..38033d65 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -21,7 +21,7 @@ import plotly.graph_objects as go import pyarrow.parquet as pq from dash import dcc, html -from dash.dependencies import Component, Input, Output, State +from dash.dependencies import Component, Input, Output from dash.exceptions import PreventUpdate from frozendict import frozendict @@ -48,6 +48,20 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 +def _resolve_cutoff(value) -> float: + """Unwrap a slider value (which may be a list) to a float, falling back + to the default cutoff.""" + if isinstance(value, list): + value = value[0] if value else DEFAULT_CUTOFF + if value is None: + return DEFAULT_CUTOFF + return float(value) + + +def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: + """Snap a clicked (pH, V) point to the precomputed grid keys.""" + return int(round(ph)), round(v * 2) / 2 + class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -143,13 +157,11 @@ def get_stable_mp_ids(self, ph: float, v: float, cutoff: float) -> list[str]: """ if self._stability_df is None: return [] - # Round to handle float-equality edge cases in the index lookup. - ph_key = int(round(ph)) - v_key = round(v * 2) / 2 # snap to 0.5 grid + ph_key, v_key = _snap_to_grid(ph, v) try: cell = self._stability_df.loc[(ph_key, v_key)] except KeyError: - logger.warning("No stability data for (pH=%s, V=%s)", ph_key, v_key) + logger.debug("No stability data for (pH=%s, V=%s)", ph_key, v_key) return [] stable = cell[cell["decomposition_energy"] <= cutoff] return stable["mp_id"].tolist() @@ -334,16 +346,10 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): if isinstance(show_water_lines, list): show_water_lines = show_water_lines[0] if show_water_lines else True - if isinstance(stability_cutoff, list): - stability_cutoff = ( - stability_cutoff[0] if stability_cutoff else DEFAULT_CUTOFF - ) - if stability_cutoff is None: - stability_cutoff = DEFAULT_CUTOFF figure = self.get_heatmap_figure( heatmap_data, - stability_cutoff=float(stability_cutoff), + stability_cutoff=_resolve_cutoff(stability_cutoff), show_water_lines=bool(show_water_lines), ) @@ -352,49 +358,4 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): figure=figure, responsive=True, config={"displayModeBar": False, "displaylogo": False}, - ) - - @app.callback( - Output(self.id("stable-mp-ids"), "data"), - Output(self.id("click-info"), "children"), - Input(self.id("heatmap"), "clickData"), - State(self.get_kwarg_id("stability_cutoff"), "value"), - ) - def on_cell_click(click_data, stability_cutoff): - if not click_data: - raise PreventUpdate - - point = click_data["points"][0] - ph = point["x"] - v = point["y"] - - if isinstance(stability_cutoff, list): - stability_cutoff = ( - stability_cutoff[0] if stability_cutoff else DEFAULT_CUTOFF - ) - if stability_cutoff is None: - stability_cutoff = DEFAULT_CUTOFF - cutoff = float(stability_cutoff) - - mp_ids = self.get_stable_mp_ids(ph, v, cutoff) - - info = html.Div( - [ - html.Div([ - "pH = ", - html.B(f"{ph}"), - ", V", - html.Sub("SHE"), - " = ", - html.B(f"{v} V"), - ]), - html.Div([ - "cutoff ≤ ", - html.B(f"{cutoff} eV/atom"), - ]), - html.Div(f"{len(mp_ids)} stable materials"), - ] - ) - - - return mp_ids, info \ No newline at end of file + ) \ No newline at end of file From f8e2b2f12c5844c4f027312819ca35a172b5c3bd Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 1 May 2026 11:54:20 -0700 Subject: [PATCH 10/33] updated examples and data --- .../apps/examples/reverse_pourbaix_example.py | 26 +- .../examples/reverse_pourbaix_heatmap.json | 1087 +++++++++++++++-- 2 files changed, 963 insertions(+), 150 deletions(-) mode change 100755 => 100644 crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py index b170c484..c6f8393a 100755 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -1,4 +1,7 @@ """Example app for the Reverse Pourbaix Diagram Component. + +Renders the heatmap statically using the component's `get_heatmap_figure` +staticmethod. No interactivity. See the Materials Project web integration. """ from __future__ import annotations @@ -7,33 +10,23 @@ from pathlib import Path import dash -from dash import html +from dash import dcc, html import crystal_toolkit.components as ctc from crystal_toolkit.settings import SETTINGS from crystal_toolkit.components.reverse_pourbaix import ReversePourbaixDiagramComponent app = dash.Dash(assets_folder=SETTINGS.ASSETS_PATH) -app.config["suppress_callback_exceptions"] = True # Load pre-computed heatmap data DATA_PATH = Path(__file__).parent / "reverse_pourbaix_heatmap.json" -print(f"Loading heatmap data from: {DATA_PATH}") - with open(DATA_PATH) as f: heatmap_data = json.load(f) -print(f" Grid points: {len(heatmap_data['grid'])}") -print(f" pH range: {heatmap_data['ph_values'][0]} to {heatmap_data['ph_values'][-1]}") -print(f" V range: {heatmap_data['v_values'][-1]} to {heatmap_data['v_values'][0]}") +# Build the heatmap figure directly — no component, no callbacks. +figure = ReversePourbaixDiagramComponent.get_heatmap_figure(heatmap_data) -# Create component -reverse_pourbaix_component = ReversePourbaixDiagramComponent( - default_data=heatmap_data -) - -# Layout layout = html.Div( [ html.H1("Reverse Pourbaix Diagram"), @@ -41,7 +34,10 @@ "Number of thermodynamically stable materials at each pH/potential " "combination (decomposition energy < 0.2 eV/atom)." ), - reverse_pourbaix_component.layout(), + dcc.Graph( + figure=figure, + config={"displayModeBar": False, "displaylogo": False}, + ), ], style=dict(maxWidth="900px", margin="2em auto"), ) @@ -49,4 +45,4 @@ ctc.register_crystal_toolkit(app=app, layout=layout) if __name__ == "__main__": - app.run(debug=True, port=8050) + app.run(debug=True, port=8050) \ No newline at end of file diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json old mode 100755 new mode 100644 index a1bf8836..423330a7 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json @@ -27,681 +27,1498 @@ -1.5, -2.0 ], + "cutoffs": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5 + ], "grid": [ { "pH": 0.0, "V": -2.0, - "count": 5620 + "counts": { + "0.1": 4434, + "0.2": 5621, + "0.3": 6819, + "0.4": 8044, + "0.5": 9464 + } }, { "pH": 0.0, "V": -1.5, - "count": 5136 + "counts": { + "0.1": 3824, + "0.2": 5137, + "0.3": 6644, + "0.4": 8830, + "0.5": 11695 + } }, { "pH": 0.0, "V": -1.0, - "count": 4664 + "counts": { + "0.1": 3039, + "0.2": 4665, + "0.3": 7275, + "0.4": 11410, + "0.5": 17082 + } }, { "pH": 0.0, "V": -0.5, - "count": 4946 + "counts": { + "0.1": 2325, + "0.2": 4948, + "0.3": 9897, + "0.4": 16961, + "0.5": 26452 + } }, { "pH": 0.0, "V": 0.0, - "count": 7209 + "counts": { + "0.1": 2070, + "0.2": 7209, + "0.3": 18500, + "0.4": 31128, + "0.5": 43152 + } }, { "pH": 0.0, "V": 0.5, - "count": 8887 + "counts": { + "0.1": 1770, + "0.2": 8887, + "0.3": 22572, + "0.4": 35679, + "0.5": 45744 + } }, { "pH": 0.0, "V": 1.0, - "count": 9633 + "counts": { + "0.1": 2093, + "0.2": 9633, + "0.3": 23973, + "0.4": 37177, + "0.5": 47804 + } }, { "pH": 0.0, "V": 1.5, - "count": 9465 + "counts": { + "0.1": 2070, + "0.2": 9465, + "0.3": 22426, + "0.4": 34763, + "0.5": 46155 + } }, { "pH": 0.0, "V": 2.0, - "count": 8651 + "counts": { + "0.1": 2382, + "0.2": 8651, + "0.3": 19244, + "0.4": 28641, + "0.5": 37909 + } }, { "pH": 1.0, "V": -2.0, - "count": 5653 + "counts": { + "0.1": 4444, + "0.2": 5654, + "0.3": 6941, + "0.4": 8188, + "0.5": 9710 + } }, { "pH": 1.0, "V": -1.5, - "count": 5277 + "counts": { + "0.1": 3856, + "0.2": 5278, + "0.3": 6967, + "0.4": 9410, + "0.5": 12625 + } }, { "pH": 1.0, "V": -1.0, - "count": 4927 + "counts": { + "0.1": 3011, + "0.2": 4928, + "0.3": 8041, + "0.4": 12735, + "0.5": 19038 + } }, { "pH": 1.0, "V": -0.5, - "count": 6009 + "counts": { + "0.1": 2616, + "0.2": 6011, + "0.3": 12098, + "0.4": 21184, + "0.5": 32751 + } }, { "pH": 1.0, "V": 0.0, - "count": 9155 + "counts": { + "0.1": 2457, + "0.2": 9155, + "0.3": 22229, + "0.4": 36190, + "0.5": 47890 + } }, { "pH": 1.0, "V": 0.5, - "count": 11351 + "counts": { + "0.1": 2489, + "0.2": 11351, + "0.3": 26402, + "0.4": 38536, + "0.5": 48721 + } }, { "pH": 1.0, "V": 1.0, - "count": 11963 + "counts": { + "0.1": 2810, + "0.2": 11963, + "0.3": 27730, + "0.4": 40567, + "0.5": 52057 + } }, { "pH": 1.0, "V": 1.5, - "count": 11736 + "counts": { + "0.1": 2728, + "0.2": 11736, + "0.3": 24895, + "0.4": 37581, + "0.5": 47677 + } }, { "pH": 1.0, "V": 2.0, - "count": 9952 + "counts": { + "0.1": 2949, + "0.2": 9952, + "0.3": 20382, + "0.4": 29538, + "0.5": 38398 + } }, { "pH": 2.0, "V": -2.0, - "count": 5713 + "counts": { + "0.1": 4448, + "0.2": 5714, + "0.3": 7049, + "0.4": 8366, + "0.5": 9998 + } }, { "pH": 2.0, "V": -1.5, - "count": 5497 + "counts": { + "0.1": 3901, + "0.2": 5498, + "0.3": 7448, + "0.4": 10201, + "0.5": 13802 + } }, { "pH": 2.0, "V": -1.0, - "count": 5344 + "counts": { + "0.1": 3041, + "0.2": 5345, + "0.3": 9127, + "0.4": 14563, + "0.5": 21440 + } }, { "pH": 2.0, "V": -0.5, - "count": 7313 + "counts": { + "0.1": 3065, + "0.2": 7315, + "0.3": 15525, + "0.4": 27192, + "0.5": 38176 + } }, { "pH": 2.0, "V": 0.0, - "count": 11461 + "counts": { + "0.1": 2788, + "0.2": 11462, + "0.3": 26986, + "0.4": 40966, + "0.5": 51160 + } }, { "pH": 2.0, "V": 0.5, - "count": 14374 + "counts": { + "0.1": 3396, + "0.2": 14374, + "0.3": 29943, + "0.4": 41860, + "0.5": 51693 + } }, { "pH": 2.0, "V": 1.0, - "count": 14728 + "counts": { + "0.1": 3514, + "0.2": 14728, + "0.3": 30979, + "0.4": 43740, + "0.5": 54564 + } }, { "pH": 2.0, "V": 1.5, - "count": 13291 + "counts": { + "0.1": 3440, + "0.2": 13291, + "0.3": 26116, + "0.4": 38464, + "0.5": 48167 + } }, { "pH": 2.0, "V": 2.0, - "count": 10378 + "counts": { + "0.1": 3193, + "0.2": 10378, + "0.3": 20903, + "0.4": 29631, + "0.5": 38169 + } }, { "pH": 3.0, "V": -2.0, - "count": 5787 + "counts": { + "0.1": 4457, + "0.2": 5788, + "0.3": 7164, + "0.4": 8549, + "0.5": 10412 + } }, { "pH": 3.0, "V": -1.5, - "count": 5800 + "counts": { + "0.1": 3999, + "0.2": 5801, + "0.3": 8031, + "0.4": 11390, + "0.5": 15041 + } }, { "pH": 3.0, "V": -1.0, - "count": 5940 + "counts": { + "0.1": 3166, + "0.2": 5941, + "0.3": 10468, + "0.4": 16483, + "0.5": 23578 + } }, { "pH": 3.0, "V": -0.5, - "count": 10015 + "counts": { + "0.1": 3752, + "0.2": 10017, + "0.3": 21299, + "0.4": 32549, + "0.5": 43230 + } }, { "pH": 3.0, "V": 0.0, - "count": 14999 + "counts": { + "0.1": 3402, + "0.2": 15000, + "0.3": 32180, + "0.4": 44577, + "0.5": 54304 + } }, { "pH": 3.0, "V": 0.5, - "count": 17533 + "counts": { + "0.1": 4414, + "0.2": 17534, + "0.3": 32935, + "0.4": 45167, + "0.5": 55494 + } }, { "pH": 3.0, "V": 1.0, - "count": 17440 + "counts": { + "0.1": 4273, + "0.2": 17440, + "0.3": 33527, + "0.4": 46955, + "0.5": 56300 + } }, { "pH": 3.0, "V": 1.5, - "count": 14434 + "counts": { + "0.1": 4089, + "0.2": 14434, + "0.3": 26821, + "0.4": 38658, + "0.5": 48094 + } }, { "pH": 3.0, "V": 2.0, - "count": 10538 + "counts": { + "0.1": 3356, + "0.2": 10538, + "0.3": 20461, + "0.4": 28871, + "0.5": 36950 + } }, { "pH": 4.0, "V": -2.0, - "count": 5852 + "counts": { + "0.1": 4473, + "0.2": 5853, + "0.3": 7252, + "0.4": 8826, + "0.5": 10748 + } }, { "pH": 4.0, "V": -1.5, - "count": 6127 + "counts": { + "0.1": 4107, + "0.2": 6128, + "0.3": 8876, + "0.4": 12383, + "0.5": 16490 + } }, { "pH": 4.0, "V": -1.0, - "count": 6780 + "counts": { + "0.1": 3331, + "0.2": 6781, + "0.3": 11893, + "0.4": 18219, + "0.5": 25547 + } }, { "pH": 4.0, "V": -0.5, - "count": 12897 + "counts": { + "0.1": 4373, + "0.2": 12899, + "0.3": 25038, + "0.4": 36203, + "0.5": 47186 + } }, { "pH": 4.0, "V": 0.0, - "count": 18848 + "counts": { + "0.1": 4417, + "0.2": 18849, + "0.3": 35651, + "0.4": 47418, + "0.5": 57138 + } }, { "pH": 4.0, "V": 0.5, - "count": 19301 + "counts": { + "0.1": 5191, + "0.2": 19302, + "0.3": 35387, + "0.4": 47680, + "0.5": 58432 + } }, { "pH": 4.0, "V": 1.0, - "count": 18614 + "counts": { + "0.1": 4920, + "0.2": 18614, + "0.3": 34981, + "0.4": 47806, + "0.5": 56958 + } }, { "pH": 4.0, "V": 1.5, - "count": 15282 + "counts": { + "0.1": 4478, + "0.2": 15282, + "0.3": 27222, + "0.4": 38435, + "0.5": 47260 + } }, { "pH": 4.0, "V": 2.0, - "count": 10773 + "counts": { + "0.1": 3811, + "0.2": 10773, + "0.3": 20317, + "0.4": 27828, + "0.5": 35626 + } }, { "pH": 5.0, "V": -2.0, - "count": 5906 + "counts": { + "0.1": 4481, + "0.2": 5907, + "0.3": 7381, + "0.4": 9102, + "0.5": 11139 + } }, { "pH": 5.0, "V": -1.5, - "count": 6611 + "counts": { + "0.1": 4268, + "0.2": 6612, + "0.3": 9683, + "0.4": 13612, + "0.5": 17957 + } }, { "pH": 5.0, "V": -1.0, - "count": 7764 + "counts": { + "0.1": 3691, + "0.2": 7765, + "0.3": 13204, + "0.4": 19764, + "0.5": 27770 + } }, { "pH": 5.0, "V": -0.5, - "count": 15315 + "counts": { + "0.1": 4971, + "0.2": 15318, + "0.3": 28201, + "0.4": 40012, + "0.5": 50513 + } }, { "pH": 5.0, "V": 0.0, - "count": 21106 + "counts": { + "0.1": 5396, + "0.2": 21107, + "0.3": 37210, + "0.4": 48797, + "0.5": 58902 + } }, { "pH": 5.0, "V": 0.5, - "count": 20598 + "counts": { + "0.1": 5912, + "0.2": 20599, + "0.3": 37490, + "0.4": 50965, + "0.5": 60314 + } }, { "pH": 5.0, "V": 1.0, - "count": 19246 + "counts": { + "0.1": 5427, + "0.2": 19246, + "0.3": 35257, + "0.4": 48041, + "0.5": 56646 + } }, { "pH": 5.0, "V": 1.5, - "count": 16042 + "counts": { + "0.1": 4947, + "0.2": 16042, + "0.3": 27544, + "0.4": 38033, + "0.5": 46617 + } }, { "pH": 5.0, "V": 2.0, - "count": 10958 + "counts": { + "0.1": 4251, + "0.2": 10958, + "0.3": 19878, + "0.4": 26876, + "0.5": 34206 + } }, { "pH": 6.0, "V": -2.0, - "count": 5985 + "counts": { + "0.1": 4496, + "0.2": 5986, + "0.3": 7579, + "0.4": 9418, + "0.5": 11585 + } }, { "pH": 6.0, "V": -1.5, - "count": 7194 + "counts": { + "0.1": 4367, + "0.2": 7195, + "0.3": 10575, + "0.4": 14757, + "0.5": 19609 + } }, { "pH": 6.0, "V": -1.0, - "count": 8524 + "counts": { + "0.1": 3923, + "0.2": 8525, + "0.3": 14283, + "0.4": 21201, + "0.5": 30302 + } }, { "pH": 6.0, "V": -0.5, - "count": 17402 + "counts": { + "0.1": 5539, + "0.2": 17405, + "0.3": 31174, + "0.4": 43205, + "0.5": 53913 + } }, { "pH": 6.0, "V": 0.0, - "count": 23256 + "counts": { + "0.1": 6474, + "0.2": 23257, + "0.3": 38880, + "0.4": 50492, + "0.5": 61779 + } }, { "pH": 6.0, "V": 0.5, - "count": 22272 + "counts": { + "0.1": 6573, + "0.2": 22273, + "0.3": 39682, + "0.4": 52941, + "0.5": 61782 + } }, { "pH": 6.0, "V": 1.0, - "count": 19627 + "counts": { + "0.1": 5676, + "0.2": 19627, + "0.3": 35338, + "0.4": 47839, + "0.5": 56304 + } }, { "pH": 6.0, "V": 1.5, - "count": 16855 + "counts": { + "0.1": 5384, + "0.2": 16855, + "0.3": 27837, + "0.4": 37295, + "0.5": 45879 + } }, { "pH": 6.0, "V": 2.0, - "count": 11107 + "counts": { + "0.1": 4641, + "0.2": 11107, + "0.3": 19189, + "0.4": 25878, + "0.5": 32723 + } }, { "pH": 7.0, "V": -2.0, - "count": 6108 + "counts": { + "0.1": 4499, + "0.2": 6109, + "0.3": 7756, + "0.4": 9779, + "0.5": 12261 + } }, { "pH": 7.0, "V": -1.5, - "count": 7773 + "counts": { + "0.1": 4666, + "0.2": 7774, + "0.3": 11546, + "0.4": 16131, + "0.5": 22026 + } }, { "pH": 7.0, "V": -1.0, - "count": 9018 + "counts": { + "0.1": 4131, + "0.2": 9019, + "0.3": 15362, + "0.4": 23215, + "0.5": 34101 + } }, { "pH": 7.0, "V": -0.5, - "count": 19190 + "counts": { + "0.1": 6144, + "0.2": 19193, + "0.3": 33651, + "0.4": 45864, + "0.5": 57640 + } }, { "pH": 7.0, "V": 0.0, - "count": 24198 + "counts": { + "0.1": 7303, + "0.2": 24199, + "0.3": 40524, + "0.4": 53152, + "0.5": 63607 + } }, { "pH": 7.0, "V": 0.5, - "count": 23848 + "counts": { + "0.1": 7285, + "0.2": 23849, + "0.3": 42204, + "0.4": 54134, + "0.5": 62400 + } }, { "pH": 7.0, "V": 1.0, - "count": 19737 + "counts": { + "0.1": 5902, + "0.2": 19737, + "0.3": 35434, + "0.4": 47270, + "0.5": 55896 + } }, { "pH": 7.0, "V": 1.5, - "count": 16200 + "counts": { + "0.1": 5250, + "0.2": 16200, + "0.3": 26836, + "0.4": 35836, + "0.5": 44649 + } }, { "pH": 7.0, "V": 2.0, - "count": 11205 + "counts": { + "0.1": 5008, + "0.2": 11205, + "0.3": 18593, + "0.4": 24962, + "0.5": 31291 + } }, { "pH": 8.0, "V": -2.0, - "count": 6245 + "counts": { + "0.1": 4539, + "0.2": 6246, + "0.3": 7995, + "0.4": 10188, + "0.5": 13197 + } }, { "pH": 8.0, "V": -1.5, - "count": 8406 + "counts": { + "0.1": 4926, + "0.2": 8407, + "0.3": 12667, + "0.4": 17896, + "0.5": 24829 + } }, { "pH": 8.0, "V": -1.0, - "count": 9765 + "counts": { + "0.1": 4467, + "0.2": 9767, + "0.3": 16868, + "0.4": 26875, + "0.5": 39381 + } }, { "pH": 8.0, "V": -0.5, - "count": 20350 + "counts": { + "0.1": 6508, + "0.2": 20353, + "0.3": 35568, + "0.4": 48979, + "0.5": 60974 + } }, { "pH": 8.0, "V": 0.0, - "count": 24214 + "counts": { + "0.1": 7422, + "0.2": 24215, + "0.3": 41154, + "0.4": 55140, + "0.5": 64149 + } }, { "pH": 8.0, "V": 0.5, - "count": 24044 + "counts": { + "0.1": 7469, + "0.2": 24045, + "0.3": 42511, + "0.4": 54536, + "0.5": 62384 + } }, { "pH": 8.0, "V": 1.0, - "count": 19461 + "counts": { + "0.1": 5912, + "0.2": 19461, + "0.3": 34562, + "0.4": 46161, + "0.5": 54917 + } }, { "pH": 8.0, "V": 1.5, - "count": 15441 + "counts": { + "0.1": 5256, + "0.2": 15441, + "0.3": 25529, + "0.4": 34422, + "0.5": 43096 + } }, { "pH": 8.0, "V": 2.0, - "count": 11106 + "counts": { + "0.1": 5220, + "0.2": 11106, + "0.3": 17829, + "0.4": 24008, + "0.5": 29894 + } }, { "pH": 9.0, "V": -2.0, - "count": 6374 + "counts": { + "0.1": 4612, + "0.2": 6375, + "0.3": 8269, + "0.4": 10974, + "0.5": 14120 + } }, { "pH": 9.0, "V": -1.5, - "count": 9069 + "counts": { + "0.1": 5241, + "0.2": 9070, + "0.3": 13705, + "0.4": 20173, + "0.5": 27182 + } }, { "pH": 9.0, "V": -1.0, - "count": 10817 + "counts": { + "0.1": 4831, + "0.2": 10819, + "0.3": 19805, + "0.4": 31816, + "0.5": 44589 + } }, { "pH": 9.0, "V": -0.5, - "count": 21450 + "counts": { + "0.1": 7147, + "0.2": 21451, + "0.3": 37950, + "0.4": 52029, + "0.5": 63819 + } }, { "pH": 9.0, "V": 0.0, - "count": 24167 + "counts": { + "0.1": 7755, + "0.2": 24168, + "0.3": 42100, + "0.4": 56021, + "0.5": 64244 + } }, { "pH": 9.0, "V": 0.5, - "count": 24018 + "counts": { + "0.1": 7739, + "0.2": 24019, + "0.3": 42235, + "0.4": 54209, + "0.5": 61974 + } }, { "pH": 9.0, "V": 1.0, - "count": 19226 + "counts": { + "0.1": 6232, + "0.2": 19226, + "0.3": 33558, + "0.4": 44680, + "0.5": 53625 + } }, { "pH": 9.0, "V": 1.5, - "count": 14887 + "counts": { + "0.1": 5446, + "0.2": 14887, + "0.3": 24415, + "0.4": 33039, + "0.5": 41295 + } }, { "pH": 9.0, "V": 2.0, - "count": 10934 + "counts": { + "0.1": 5385, + "0.2": 10934, + "0.3": 17085, + "0.4": 23093, + "0.5": 28637 + } }, { "pH": 10.0, "V": -2.0, - "count": 6344 + "counts": { + "0.1": 4575, + "0.2": 6345, + "0.3": 8570, + "0.4": 11635, + "0.5": 15160 + } }, { "pH": 10.0, "V": -1.5, - "count": 9418 + "counts": { + "0.1": 5379, + "0.2": 9419, + "0.3": 15092, + "0.4": 22116, + "0.5": 29091 + } }, { "pH": 10.0, "V": -1.0, - "count": 12967 + "counts": { + "0.1": 5407, + "0.2": 12969, + "0.3": 24438, + "0.4": 36887, + "0.5": 49449 + } }, { "pH": 10.0, "V": -0.5, - "count": 22788 + "counts": { + "0.1": 7682, + "0.2": 22789, + "0.3": 40533, + "0.4": 54499, + "0.5": 65750 + } }, { "pH": 10.0, "V": 0.0, - "count": 24236 + "counts": { + "0.1": 7972, + "0.2": 24237, + "0.3": 42595, + "0.4": 55815, + "0.5": 63884 + } }, { "pH": 10.0, "V": 0.5, - "count": 23772 + "counts": { + "0.1": 7720, + "0.2": 23773, + "0.3": 41540, + "0.4": 53350, + "0.5": 61264 + } }, { "pH": 10.0, "V": 1.0, - "count": 18824 + "counts": { + "0.1": 6383, + "0.2": 18824, + "0.3": 32649, + "0.4": 43132, + "0.5": 52024 + } }, { "pH": 10.0, "V": 1.5, - "count": 14025 + "counts": { + "0.1": 5453, + "0.2": 14025, + "0.3": 23111, + "0.4": 31410, + "0.5": 39010 + } }, { "pH": 10.0, "V": 2.0, - "count": 10692 + "counts": { + "0.1": 5511, + "0.2": 10692, + "0.3": 16419, + "0.4": 22239, + "0.5": 27526 + } }, { "pH": 11.0, "V": -2.0, - "count": 6379 + "counts": { + "0.1": 4508, + "0.2": 6380, + "0.3": 9120, + "0.4": 12465, + "0.5": 16102 + } }, { "pH": 11.0, "V": -1.5, - "count": 10004 + "counts": { + "0.1": 5268, + "0.2": 10006, + "0.3": 16470, + "0.4": 23612, + "0.5": 30754 + } }, { "pH": 11.0, "V": -1.0, - "count": 14115 + "counts": { + "0.1": 5582, + "0.2": 14117, + "0.3": 26859, + "0.4": 39691, + "0.5": 52220 + } }, { "pH": 11.0, "V": -0.5, - "count": 24216 + "counts": { + "0.1": 7869, + "0.2": 24217, + "0.3": 42027, + "0.4": 55797, + "0.5": 66566 + } }, { "pH": 11.0, "V": 0.0, - "count": 23808 + "counts": { + "0.1": 7641, + "0.2": 23809, + "0.3": 42512, + "0.4": 55325, + "0.5": 63201 + } }, { "pH": 11.0, "V": 0.5, - "count": 23341 + "counts": { + "0.1": 7281, + "0.2": 23342, + "0.3": 40593, + "0.4": 51993, + "0.5": 60245 + } }, { "pH": 11.0, "V": 1.0, - "count": 18346 + "counts": { + "0.1": 6260, + "0.2": 18346, + "0.3": 31744, + "0.4": 41734, + "0.5": 50320 + } }, { "pH": 11.0, "V": 1.5, - "count": 13045 + "counts": { + "0.1": 5221, + "0.2": 13045, + "0.3": 21845, + "0.4": 29597, + "0.5": 37176 + } }, { "pH": 11.0, "V": 2.0, - "count": 10221 + "counts": { + "0.1": 5327, + "0.2": 10221, + "0.3": 15672, + "0.4": 21346, + "0.5": 26531 + } }, { "pH": 12.0, "V": -2.0, - "count": 6623 + "counts": { + "0.1": 4479, + "0.2": 6624, + "0.3": 9659, + "0.4": 13262, + "0.5": 17159 + } }, { "pH": 12.0, "V": -1.5, - "count": 10921 + "counts": { + "0.1": 5306, + "0.2": 10923, + "0.3": 17407, + "0.4": 24797, + "0.5": 32214 + } }, { "pH": 12.0, "V": -1.0, - "count": 15518 + "counts": { + "0.1": 5753, + "0.2": 15520, + "0.3": 28977, + "0.4": 42028, + "0.5": 54440 + } }, { "pH": 12.0, "V": -0.5, - "count": 23426 + "counts": { + "0.1": 7229, + "0.2": 23428, + "0.3": 41080, + "0.4": 55171, + "0.5": 65287 + } }, { "pH": 12.0, "V": 0.0, - "count": 23347 + "counts": { + "0.1": 7248, + "0.2": 23348, + "0.3": 41763, + "0.4": 54329, + "0.5": 62388 + } }, { "pH": 12.0, "V": 0.5, - "count": 22856 + "counts": { + "0.1": 6853, + "0.2": 22856, + "0.3": 39215, + "0.4": 50321, + "0.5": 58870 + } }, { "pH": 12.0, "V": 1.0, - "count": 17245 + "counts": { + "0.1": 5936, + "0.2": 17245, + "0.3": 29954, + "0.4": 39968, + "0.5": 48205 + } }, { "pH": 12.0, "V": 1.5, - "count": 12146 + "counts": { + "0.1": 5106, + "0.2": 12146, + "0.3": 20624, + "0.4": 27920, + "0.5": 35333 + } }, { "pH": 12.0, "V": 2.0, - "count": 9489 + "counts": { + "0.1": 5156, + "0.2": 9489, + "0.3": 14728, + "0.4": 20340, + "0.5": 25380 + } }, { "pH": 13.0, "V": -2.0, - "count": 7041 + "counts": { + "0.1": 4501, + "0.2": 7042, + "0.3": 10465, + "0.4": 14305, + "0.5": 18358 + } }, { "pH": 13.0, "V": -1.5, - "count": 11472 + "counts": { + "0.1": 5628, + "0.2": 11474, + "0.3": 18087, + "0.4": 25546, + "0.5": 33826 + } }, { "pH": 13.0, "V": -1.0, - "count": 16400 + "counts": { + "0.1": 5614, + "0.2": 16402, + "0.3": 30728, + "0.4": 43535, + "0.5": 55510 + } }, { "pH": 13.0, "V": -0.5, - "count": 21638 + "counts": { + "0.1": 6499, + "0.2": 21639, + "0.3": 39549, + "0.4": 54048, + "0.5": 63601 + } }, { "pH": 13.0, "V": 0.0, - "count": 22250 + "counts": { + "0.1": 6937, + "0.2": 22251, + "0.3": 40514, + "0.4": 52931, + "0.5": 61330 + } }, { "pH": 13.0, "V": 0.5, - "count": 21634 + "counts": { + "0.1": 6550, + "0.2": 21634, + "0.3": 37526, + "0.4": 48177, + "0.5": 56671 + } }, { "pH": 13.0, "V": 1.0, - "count": 15533 + "counts": { + "0.1": 5434, + "0.2": 15533, + "0.3": 27707, + "0.4": 37362, + "0.5": 45339 + } }, { "pH": 13.0, "V": 1.5, - "count": 11052 + "counts": { + "0.1": 4884, + "0.2": 11052, + "0.3": 19103, + "0.4": 25866, + "0.5": 33147 + } }, { "pH": 13.0, "V": 2.0, - "count": 8678 + "counts": { + "0.1": 4998, + "0.2": 8678, + "0.3": 13741, + "0.4": 19263, + "0.5": 24133 + } }, { "pH": 14.0, "V": -2.0, - "count": 7769 + "counts": { + "0.1": 4537, + "0.2": 7770, + "0.3": 11448, + "0.4": 15576, + "0.5": 20015 + } }, { "pH": 14.0, "V": -1.5, - "count": 11383 + "counts": { + "0.1": 5416, + "0.2": 11385, + "0.3": 18043, + "0.4": 25791, + "0.5": 35107 + } }, { "pH": 14.0, "V": -1.0, - "count": 16389 + "counts": { + "0.1": 5528, + "0.2": 16391, + "0.3": 30722, + "0.4": 43706, + "0.5": 56402 + } }, { "pH": 14.0, "V": -0.5, - "count": 19883 + "counts": { + "0.1": 5999, + "0.2": 19884, + "0.3": 37429, + "0.4": 52302, + "0.5": 62172 + } }, { "pH": 14.0, "V": 0.0, - "count": 20626 + "counts": { + "0.1": 6412, + "0.2": 20627, + "0.3": 38600, + "0.4": 50844, + "0.5": 59654 + } }, { "pH": 14.0, "V": 0.5, - "count": 20014 + "counts": { + "0.1": 5971, + "0.2": 20014, + "0.3": 35275, + "0.4": 45868, + "0.5": 54328 + } }, { "pH": 14.0, "V": 1.0, - "count": 13583 + "counts": { + "0.1": 4888, + "0.2": 13583, + "0.3": 25291, + "0.4": 34560, + "0.5": 42304 + } }, { "pH": 14.0, "V": 1.5, - "count": 10060 + "counts": { + "0.1": 4681, + "0.2": 10060, + "0.3": 17537, + "0.4": 24170, + "0.5": 30658 + } }, { "pH": 14.0, "V": 2.0, - "count": 8005 + "counts": { + "0.1": 4801, + "0.2": 8005, + "0.3": 12732, + "0.4": 17965, + "0.5": 22797 + } } ] } \ No newline at end of file From 3ae396a09c2d4590d90c30ac284a3bb85245640f Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 4 May 2026 11:38:06 -0700 Subject: [PATCH 11/33] updated comments --- crystal_toolkit/components/reverse_pourbaix.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 38033d65..c73ab9c4 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -7,8 +7,6 @@ This is the "reverse" of the standard Pourbaix diagram: instead of showing stability domains for a single material, it shows how many materials are stable at each electrochemical condition. - -See: Karlsson et al., Electrochimica Acta 549 (2026) 148053 """ from __future__ import annotations @@ -42,8 +40,7 @@ MAX_V = 2 # Stability cutoff (eV/atom) — the recommended practical value is 0.2, -# matching the metastability threshold used in Karlsson et al. and the -# upstream MP/Sun/Aykol references. +# up to 0.5 also makes sense DEFAULT_CUTOFF = 0.2 CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 @@ -68,8 +65,7 @@ class ReversePourbaixDiagramComponent(MPComponent): Shows a heatmap of the number of stable materials at each pH/V combination, where stability is defined by a user-tunable - decomposition energy cutoff (eV/atom). Clicking on a cell exposes - the list of mp_ids stable at that condition for downstream use. + decomposition energy cutoff (eV/atom). """ default_state = frozendict( @@ -128,7 +124,11 @@ def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): parquet_path: path to the precomputed (pH, V, mp_id, decomposition_energy) parquet file. Loaded once at component construction. If None, the click-to-list functionality is disabled but the heatmap still works. + Current parquet data is computed with solid filter and default + ion concentrations. """ + # TODO: in future, it would be nice if user can disable solid filter and + # specify ion concentrations. super().__init__(*args, **kwargs) self._stability_df: pd.DataFrame | None = None if parquet_path is not None: @@ -271,10 +271,7 @@ def _sub_layouts(self) -> dict[str, Component]: # Downstream callbacks (filtering, table rendering, etc.) can read this. mp_id_store = dcc.Store(id=self.id("stable-mp-ids"), data=[]) - # Selection panel — mirrors the panel structure the app uses for - # Options so they render identically when stacked. The click callback - # writes to the inner content div (id "click-info"); the outer panel - # chrome is static. + # Selection panel info = html.Div( [ html.Div("Selected conditions", className="panel-heading"), From 8c596a2e512a09c30efc59a799f7e6f65f666290 Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 4 May 2026 11:53:10 -0700 Subject: [PATCH 12/33] Revert unrelated changes for demo branch --- crystal_toolkit/apps/examples/pourbaix.py | 7 +- crystal_toolkit/components/pourbaix.py | 123 +++++++------ pyproject.toml | 2 +- requirements/ubuntu-latest_py3.11.txt | 100 ++++++----- requirements/ubuntu-latest_py3.11_extras.txt | 179 ++++++++++--------- requirements/ubuntu-latest_py3.12.txt | 102 ++++++----- requirements/ubuntu-latest_py3.12_extras.txt | 178 +++++++++--------- 7 files changed, 362 insertions(+), 329 deletions(-) diff --git a/crystal_toolkit/apps/examples/pourbaix.py b/crystal_toolkit/apps/examples/pourbaix.py index 549046e4..56941932 100644 --- a/crystal_toolkit/apps/examples/pourbaix.py +++ b/crystal_toolkit/apps/examples/pourbaix.py @@ -2,8 +2,7 @@ import dash from dash import html -from mp_api.client import MPRester -import os +from pymatgen.ext.matproj import MPRester import crystal_toolkit.components as ctc from crystal_toolkit.settings import SETTINGS @@ -16,10 +15,8 @@ # for more information. app.config["suppress_callback_exceptions"] = True -print("API_Key found:", os.environ.get("MP_API_KEY")) - # first, retrieve entries from Materials Project -with MPRester() as mpr: #os.environ.get("MP_API_KEY") +with MPRester() as mpr: pourbaix_entries = mpr.get_pourbaix_entries("Fe-Co") pourbaix_component = ctc.PourbaixDiagramComponent(default_data=pourbaix_entries) diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index ce13dfa1..64cc652f 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -17,7 +17,7 @@ from shapely.geometry import Polygon import crystal_toolkit.helpers.layouts as ctl -from crystal_toolkit.components.error_msg import ErrorMessageAIO +from crystal_toolkit.components.messageAIO import MessageAIO from crystal_toolkit.core.mpcomponent import MPComponent try: @@ -459,45 +459,61 @@ def _sub_layouts(self) -> dict[str, Component]: ), html.Div( [ - ErrorMessageAIO( - "Invalid composition input!", - aio_id=self.id("invalid-comp-alarm"), - msg_type="error", - ), - ErrorMessageAIO( - "Invalid concentration input!", - aio_id=self.id("invalid-conc-alarm"), - msg_type="error", + MessageAIO( + aio_id=self.id("outputConsole"), ), html.Div( [ - ctl.add_label_help( - html.Div(), - label="Composition Control", - help="Set the relative composition of non-H/O elements.", - ), html.Div( - id=self.id("composition-title"), - style={"marginBottom": "0.5rem"}, - ), - dcc.Input( - id=self.id("comp-text"), - className="input", - type="text", + [ + html.H5( + "Composition Control", + style={ + "fontWeight": "bold", + "textAlign": "center", + "flex": "0 0 100%", + }, + ), + html.H5( + "Composition of", + id=self.id("composition-title"), + # style={"fontWeight": "bold"}, + ), + ], style={ - "textAlign": "center", - "width": "10rem", - "marginRight": "0.2rem", - "marginBottom": "0.2rem", - "height": "36px", - "fontSize": "14px", + "line-height": PANEL_LINE_HEIGHT, + "display": "flex", + "flexWrap": "wrap", + "justifyContent": "center", }, ), + PourbaixDiagramComponent.create_centered_object( + dcc.Input( + id=self.id("comp-text"), + className="input", + type="text", + style={ + "textAlign": "center", + "width": "10rem", + "marginRight": "0.2rem", + "marginBottom": "0.2rem", + "height": "36px", + "fontSize": "14px", + }, + ), + ), ctl.Block( - html.Div( - id=self.id("display-composition"), + PourbaixDiagramComponent.create_centered_object( + html.Div( + id=self.id("display-composition"), + ) ) ), + html.Hr( + style={ + "backgroundColor": "#C5C5C6", + } + ), dcc.Store(id=self.id("elements-store")), ], id=self.id("comp-panel"), @@ -520,7 +536,7 @@ def _sub_layouts(self) -> dict[str, Component]: ), ], style={ - "backgroundColor": "#FFFFFF", + "backgroundColor": "#F1F1F5", }, ), html.Br(), @@ -598,9 +614,6 @@ def update_heatmap_choices(entries, mat_detials, filter_solids): entry for entry in entries_obj if entry.phase_type == "Solid" ] - #NOTE: filter_solids bool affects both heatmap choices and decomposition energy. - # Some user may want to find the decomposition eneryg of an mp-id (which is skipped in the choices) while creating the actual pbx with filter_solids = True. - # Pymatgen allows for this functionality, but currently this pourbaix UI does not allow for it. if filter_solids: # O is 2.46 b/c pbx entry finds energies referenced to H2O entries_HO = [ComputedEntry("H", 0), ComputedEntry("O", 2.46)] @@ -694,17 +707,19 @@ def update_element_specific_sliders( conc_inputs = [] for element in sorted(elements): - conc_input = self.get_numerical_input( + conc_input = PourbaixDiagramComponent.create_centered_object( + self.get_numerical_input( f"conc-{element}", default=1e-6, min=MIN_CONCENTRATION, max=MAX_CONCENTRATION, - label=f"Concentration of {element} ion", + label=f"concentration of {element} ion", style={ "width": "10rem", "fontSize": "14px", }, ) + ) conc_inputs.append(conc_input) @@ -717,12 +732,18 @@ def update_element_specific_sliders( ) comp_conc_controls.append( - ctl.add_label_help( - html.Div(), - label=ion_label, - help=f"Equilibrium concentration beyond this threshold is considered unstable (meaningfully dissolved). Set the range between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} (M)", + html.H5( + ion_label, + style={"fontWeight": "bold", "textAlign": "center"}, ), ) + comp_conc_controls.append( + PourbaixDiagramComponent.create_centered_object( + html.H6( + f"💡 Set the range between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} (M)" + ) + ) + ) comp_conc_controls += conc_inputs @@ -740,12 +761,12 @@ def update_element_specific_sliders( default_comp = ":".join(["1" for _ in elements]) # composition title - title = "Composition of " + ":".join(elements) + title = "💡 Composition of " + ":".join(elements) # update_string update_string = "Concentration update" if len(elements) > 1: - update_string = "Update composition & concentrations" + update_string = "Composition & concentration update" return ( html.Div( @@ -768,8 +789,7 @@ def get_pourbaix_diagram(pourbaix_entries, **kwargs): @app.callback( Output(self.id("graph-panel"), "children"), - Output(ErrorMessageAIO.ids.visible(self.id("invalid-comp-alarm")), "data"), - Output(ErrorMessageAIO.ids.visible(self.id("invalid-conc-alarm")), "data"), + Output(MessageAIO.ids.data(self.id("outputConsole")), "data"), Output(self.id("display-composition"), "children"), Input(self.id(), "data"), Input(self.id("display-composition"), "children"), @@ -808,8 +828,7 @@ def make_figure( logger.error("Invalid composition input!") return ( self.get_figure_div(), - True, - False, + {"message": "Invalid composition input!", "msg_type": "error"}, "", ) try: @@ -825,8 +844,7 @@ def make_figure( logger.error("Invalid composition input!") return ( self.get_figure_div(), - True, - False, + {"message": "Invalid composition input!", "msg_type": "error"}, "", ) @@ -861,8 +879,10 @@ def make_figure( # if the input is out of pre-defined range, Input will get None return ( self.get_figure_div(), - False, - True, + { + "message": "Invalid concentration input!", + "msg_type": "error", + }, "", ) @@ -891,7 +911,6 @@ def make_figure( return ( self.get_figure_div(figure=figure), - False, - False, + {}, html.Small(f"Pourbaix composition set to {unicodeify(formula)}."), ) diff --git a/pyproject.toml b/pyproject.toml index a950f3ff..67f718d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.10" authors = [{ name = "Matt Horton", email = "mkhorton@lbl.gov" }] dependencies = [ - "dash-mp-components>=0.5.1rc5,<0.5.2", + "dash-mp-components>=0.5.1rc9,<0.5.2", "dash>=2.11.0,<4.0.0", # Issue in dash==4.0.0 with multi-select drop down: https://community.plotly.com/t/multi-select-dropdown-options-do-not-update-correct-when-labels-have-html-component-in-dash-4-0-0/96495 "flask-caching", "frozendict", diff --git a/requirements/ubuntu-latest_py3.11.txt b/requirements/ubuntu-latest_py3.11.txt index 7d4883d8..c76b6145 100644 --- a/requirements/ubuntu-latest_py3.11.txt +++ b/requirements/ubuntu-latest_py3.11.txt @@ -9,24 +9,24 @@ annotated-types==0.7.0 asttokens==3.0.1 # via stack-data bibtexparser==1.4.4 - # via pymatgen + # via pymatgen-core blake3==1.0.8 # via emmet-core blinker==1.9.0 # via flask -boto3==1.42.68 +boto3==1.42.95 # via mp-api -botocore==1.42.68 +botocore==1.42.95 # via # boto3 # s3transfer cachelib==0.13.0 # via flask-caching -certifi==2026.2.25 +certifi==2026.4.22 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via flask contourpy==1.3.3 # via matplotlib @@ -34,7 +34,7 @@ cycler==0.12.1 # via matplotlib dash==3.4.0 # via crystal_toolkit (pyproject.toml) -dash-mp-components==0.5.1rc5 +dash-mp-components==0.5.1rc9 # via crystal_toolkit (pyproject.toml) decorator==5.2.1 # via ipython @@ -46,19 +46,19 @@ flask==3.1.3 # via # dash # flask-caching -flask-caching==2.3.1 +flask-caching==2.4.0 # via crystal_toolkit (pyproject.toml) fonttools==4.62.1 # via matplotlib frozendict==2.4.7 # via crystal_toolkit (pyproject.toml) -idna==3.11 +idna==3.13 # via requests imageio==2.37.3 # via scikit-image -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via dash -ipython==9.10.0 +ipython==9.13.0 # via crystal_toolkit (pyproject.toml) ipython-pygments-lexers==1.1.1 # via ipython @@ -74,7 +74,7 @@ jmespath==1.1.0 # botocore joblib==1.5.3 # via - # pymatgen + # pymatgen-core # scikit-learn kiwisolver==1.5.0 # via matplotlib @@ -82,40 +82,42 @@ latexcodec==3.0.1 # via pybtex lazy-loader==0.5 # via scikit-image +lxml==6.1.0 + # via pymatgen-core markupsafe==3.0.3 # via # flask # jinja2 # werkzeug -matplotlib==3.10.8 - # via pymatgen +matplotlib==3.10.9 + # via pymatgen-core matplotlib-inline==0.2.1 # via ipython monty==2026.2.18 # via # emmet-core # mp-api - # pymatgen + # pymatgen-core mp-api==0.46.0 # via crystal_toolkit (pyproject.toml) mpmath==1.3.0 # via sympy -narwhals==2.18.0 +narwhals==2.20.0 # via plotly nest-asyncio==1.6.0 # via dash networkx==3.6.1 # via - # pymatgen + # pymatgen-core # scikit-image -numpy==2.4.3 +numpy==2.4.4 # via # contourpy # imageio # matplotlib # monty # pandas - # pymatgen + # pymatgen-core # pymatgen-io-validation # scikit-image # scikit-learn @@ -123,63 +125,67 @@ numpy==2.4.3 # shapely # spglib # tifffile -orjson==3.11.7 +orjson==3.11.8 # via # mp-api - # pymatgen -packaging==26.0 + # pymatgen-core +packaging==26.1 # via # lazy-loader # matplotlib # plotly # scikit-image palettable==3.3.3 - # via pymatgen -pandas==3.0.1 - # via pymatgen + # via pymatgen-core +pandas==3.0.2 + # via pymatgen-core parso==0.8.6 # via jedi pexpect==4.9.0 # via ipython -pillow==12.1.1 +pillow==12.2.0 # via # imageio # matplotlib # scikit-image -plotly==6.6.0 +plotly==6.7.0 # via # dash - # pymatgen + # pymatgen-core prompt-toolkit==3.0.52 # via ipython +psutil==7.2.2 + # via ipython ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pybtex==0.25.1 +pybtex==0.26.1 # via emmet-core -pydantic==2.12.5 +pydantic==2.13.3 # via # emmet-core # pydantic-settings # pymatgen-io-validation -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic -pydantic-settings==2.13.1 +pydantic-settings==2.14.0 # via # crystal_toolkit (pyproject.toml) # emmet-core # pymatgen-io-validation -pygments==2.19.2 +pygments==2.20.0 # via # ipython # ipython-pygments-lexers -pymatgen==2025.10.7 +pymatgen==2026.3.23 # via # crystal_toolkit (pyproject.toml) # emmet-core # mp-api # pymatgen-io-validation +pymatgen-core==2026.4.16 + # via pymatgen pymatgen-io-validation==0.1.2 # via emmet-core pyparsing==3.3.2 @@ -195,19 +201,17 @@ python-dotenv==1.2.2 # via pydantic-settings pyyaml==6.0.3 # via pybtex -requests==2.32.5 +requests==2.33.1 # via # dash # mp-api - # pymatgen + # pymatgen-core # pymatgen-io-validation retrying==1.4.2 # via dash ruamel-yaml==0.19.1 - # via - # monty - # pymatgen -s3transfer==0.16.0 + # via monty +s3transfer==0.16.1 # via boto3 scikit-image==0.26.0 # via crystal_toolkit (pyproject.toml) @@ -215,7 +219,7 @@ scikit-learn==1.8.0 # via crystal_toolkit (pyproject.toml) scipy==1.17.1 # via - # pymatgen + # pymatgen-core # scikit-image # scikit-learn shapely==2.1.2 @@ -223,19 +227,19 @@ shapely==2.1.2 six==1.17.0 # via python-dateutil spglib==2.7.0 - # via pymatgen + # via pymatgen-core stack-data==0.6.3 # via ipython sympy==1.14.0 - # via pymatgen + # via pymatgen-core tabulate==0.10.0 - # via pymatgen + # via pymatgen-core threadpoolctl==3.6.0 # via scikit-learn tifffile==2026.3.3 # via scikit-image tqdm==4.67.3 - # via pymatgen + # via pymatgen-core traitlets==5.14.3 # via # ipython @@ -256,7 +260,7 @@ typing-inspection==0.4.2 # pydantic # pydantic-settings uncertainties==3.2.3 - # via pymatgen + # via pymatgen-core urllib3==2.6.3 # via # botocore @@ -265,11 +269,11 @@ wcwidth==0.6.0 # via prompt-toolkit webcolors==25.10.0 # via crystal_toolkit (pyproject.toml) -werkzeug==3.1.6 +werkzeug==3.1.8 # via # dash # flask -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/ubuntu-latest_py3.11_extras.txt b/requirements/ubuntu-latest_py3.11_extras.txt index d31843fa..faafdf02 100644 --- a/requirements/ubuntu-latest_py3.11_extras.txt +++ b/requirements/ubuntu-latest_py3.11_extras.txt @@ -8,7 +8,7 @@ aiofiles==22.1.0 # via ypy-websocket aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.3 +aiohttp==3.13.5 # via dephell aiosignal==1.4.0 # via aiohttp @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -anyio==4.12.1 +anyio==4.13.0 # via jupyter-server argon2-cffi==25.1.0 # via @@ -28,14 +28,14 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi arrow==1.4.0 # via isoduration -ase==3.27.0 +ase==3.28.0 # via # boltztrap2 # dscribe # hiphive asttokens==3.0.1 # via stack-data -attrs==25.4.0 +attrs==26.1.0 # via # aiohttp # dephell @@ -59,7 +59,7 @@ beautifulsoup4==4.14.3 # gdown # nbconvert bibtexparser==1.4.4 - # via pymatgen + # via pymatgen-core black==26.3.1 # via crystal_toolkit (pyproject.toml) blake3==1.0.8 @@ -72,9 +72,9 @@ boltztrap2==25.3.1 # via # crystal_toolkit (pyproject.toml) # ifermi -boto3==1.42.68 +boto3==1.42.95 # via mp-api -botocore==1.42.68 +botocore==1.42.95 # via # boto3 # s3transfer @@ -82,7 +82,7 @@ cachelib==0.13.0 # via flask-caching cerberus==1.3.8 # via dephell -certifi==2026.2.25 +certifi==2026.4.22 # via # dephell # netcdf4 @@ -95,11 +95,11 @@ cfgv==3.5.0 # via pre-commit cftime==1.6.5 # via netcdf4 -charset-normalizer==3.4.5 +charset-normalizer==3.4.7 # via requests choreographer==1.2.1 # via kaleido -click==8.3.1 +click==8.3.3 # via # black # dask @@ -118,9 +118,9 @@ commonmark==0.9.1 # via recommonmark contourpy==1.3.3 # via matplotlib -coverage[toml]==7.13.4 +coverage[toml]==7.13.5 # via pytest-cov -cryptography==46.0.5 +cryptography==46.0.7 # via dash crystaltoolkit-extension==0.6.0 # via crystal_toolkit (pyproject.toml) @@ -135,13 +135,13 @@ dash[testing]==3.4.0 # dash-vtk dash-extensions==2.0.5 # via crystal_toolkit (pyproject.toml) -dash-mp-components==0.5.1rc5 +dash-mp-components==0.5.1rc9 # via crystal_toolkit (pyproject.toml) dash-testing-stub==0.0.2 # via dash dash-vtk==0.0.9 # via crystal_toolkit (pyproject.toml) -dask==2026.1.2 +dask==2026.3.0 # via # distributed # py4dstem @@ -192,7 +192,7 @@ dill==0.4.1 # py4dstem distlib==0.4.0 # via virtualenv -distributed==2026.1.2 +distributed==2026.3.0 # via py4dstem dnspython==2.8.0 # via pymongo @@ -216,7 +216,7 @@ executing==2.2.1 # via stack-data fastjsonschema==2.21.2 # via nbformat -filelock==3.25.2 +filelock==3.29.0 # via # gdown # python-discovery @@ -225,7 +225,7 @@ flask==3.1.3 # via # dash # flask-caching -flask-caching==2.3.1 +flask-caching==2.4.0 # via # crystal_toolkit (pyproject.toml) # dash-extensions @@ -239,17 +239,17 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal -fsspec==2026.2.0 +fsspec==2026.3.0 # via dask -gdown==5.2.1 +gdown==6.0.0 # via py4dstem -gevent==25.9.1 +gevent==26.4.0 # via gunicorn -greenlet==3.3.2 +greenlet==3.4.0 # via # gevent # playwright -gunicorn[gevent]==25.1.0 +gunicorn[gevent]==25.3.0 # via crystal_toolkit (pyproject.toml) h5py==3.16.0 # via @@ -265,9 +265,9 @@ hdf5plugin==6.0.0 # via py4dstem hiphive==1.5 # via crystal_toolkit (pyproject.toml) -identify==2.6.17 +identify==2.6.19 # via pre-commit -idna==3.11 +idna==3.13 # via # anyio # jsonschema @@ -279,7 +279,7 @@ imageio==2.37.3 # via scikit-image imagesize==2.0.0 # via sphinx -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via # dash # dask @@ -291,7 +291,7 @@ ipykernel==6.29.5 # via # nbclassic # notebook -ipython==9.10.0 +ipython==9.13.0 # via # crystal_toolkit (pyproject.toml) # ipykernel @@ -327,14 +327,14 @@ jmespath==1.1.0 joblib==1.5.3 # via # dscribe - # pymatgen + # pymatgen-core # scikit-learn # scikit-optimize jsbeautifier==1.15.4 # via dash-extensions -json5==0.13.0 +json5==0.14.0 # via jupyterlab-server -jsonpointer==3.0.0 +jsonpointer==3.1.1 # via jsonschema jsonschema[format-nongpl]==4.26.0 # via @@ -359,7 +359,7 @@ jupyter-core==5.9.1 # nbconvert # nbformat # notebook -jupyter-events==0.12.0 +jupyter-events==0.12.1 # via # jupyter-server # jupyter-server-fileid @@ -395,7 +395,7 @@ latexcodec==3.0.1 # via pybtex lazy-loader==0.5 # via scikit-image -llvmlite==0.46.0 +llvmlite==0.47.0 # via numba locket==1.0.0 # via @@ -405,8 +405,10 @@ logistro==2.0.1 # via # choreographer # kaleido -lxml==6.0.2 - # via dash +lxml==6.1.0 + # via + # dash + # pymatgen-core m2r==0.2.1 # via dephell markupsafe==3.0.3 @@ -415,9 +417,9 @@ markupsafe==3.0.3 # jinja2 # nbconvert # werkzeug -matminer==0.10.0 +matminer==0.10.1 # via robocrys -matplotlib==3.10.8 +matplotlib==3.10.9 # via # ase # boltztrap2 @@ -425,7 +427,7 @@ matplotlib==3.10.8 # ncempy # phonopy # py4dstem - # pymatgen + # pymatgen-core # vtk matplotlib-inline==0.2.1 # via @@ -443,9 +445,9 @@ monty==2026.2.18 # ifermi # matminer # mp-api - # pymatgen + # pymatgen-core # robocrys -more-itertools==10.8.0 +more-itertools==11.0.2 # via # dash-extensions # inflect @@ -465,7 +467,7 @@ multiprocess==0.70.19 # via dash mypy-extensions==1.1.0 # via black -narwhals==2.18.0 +narwhals==2.20.0 # via plotly nbclassic==1.3.3 # via @@ -473,7 +475,7 @@ nbclassic==1.3.3 # notebook nbclient==0.10.4 # via nbconvert -nbconvert==7.17.0 +nbconvert==7.17.1 # via # jupyter-server # notebook @@ -497,7 +499,7 @@ netcdf4==1.7.4 networkx==3.6.1 # via # ifermi - # pymatgen + # pymatgen-core # robocrys # scikit-image nodeenv==1.10.0 @@ -506,7 +508,7 @@ notebook==6.5.7 # via jupyterlab notebook-shim==0.2.4 # via nbclassic -numba==0.64.0 +numba==0.65.1 # via # hiphive # sparse @@ -536,7 +538,7 @@ numpy==1.26.4 # py4dstem # pyfftw # pylops - # pymatgen + # pymatgen-core # pymatgen-io-validation # robocrys # scikit-image @@ -552,14 +554,14 @@ numpy==1.26.4 # trimesh numpy-stl==3.2.0 # via meshcut -orjson==3.11.7 +orjson==3.11.8 # via # kaleido # mp-api - # pymatgen + # pymatgen-core overrides==7.7.0 # via jupyter-server -packaging==26.0 +packaging==26.1 # via # black # dask @@ -585,19 +587,19 @@ packaging==26.0 # scikit-optimize # sphinx palettable==3.3.3 - # via pymatgen + # via pymatgen-core pandas==2.3.3 # via # hiphive # matminer - # pymatgen + # pymatgen-core pandocfilters==1.5.1 # via nbconvert parso==0.8.6 # via jedi partd==1.4.2 # via dask -pathspec==1.0.4 +pathspec==1.1.0 # via black percy==2.0.2 # via dash @@ -605,14 +607,14 @@ pexpect==4.9.0 # via # dephell-shells # ipython -phonopy==3.1.0 +phonopy==3.5.1 # via crystal_toolkit (pyproject.toml) -pillow==12.1.1 +pillow==12.2.0 # via # imageio # matplotlib # scikit-image -platformdirs==4.9.4 +platformdirs==4.9.6 # via # black # jupyter-core @@ -622,18 +624,18 @@ playwright==1.58.0 # via # crystal_toolkit (pyproject.toml) # pytest-playwright -plotly==6.6.0 +plotly==6.7.0 # via # dash # ifermi - # pymatgen + # pymatgen-core pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.5.1 +pre-commit==4.6.0 # via crystal_toolkit (pyproject.toml) -prometheus-client==0.24.1 +prometheus-client==0.25.0 # via # jupyter-server # notebook @@ -649,6 +651,7 @@ psutil==7.2.2 # distributed # ifermi # ipykernel + # ipython ptyprocess==0.7.0 # via # pexpect @@ -661,21 +664,21 @@ py4dstem==0.14.18 # via crystal_toolkit (pyproject.toml) pyaml==26.2.1 # via scikit-optimize -pybtex==0.25.1 +pybtex==0.26.1 # via # emmet-core # robocrys pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via # dash-extensions # emmet-core # pydantic-settings # pymatgen-io-validation -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic -pydantic-settings==2.13.1 +pydantic-settings==2.14.0 # via # crystal_toolkit (pyproject.toml) # emmet-core @@ -686,7 +689,7 @@ pyfftw==0.15.1 # via # crystal_toolkit (pyproject.toml) # ifermi -pygments==2.19.2 +pygments==2.20.0 # via # ipython # ipython-pygments-lexers @@ -696,7 +699,7 @@ pygments==2.19.2 # sphinx pylops==2.6.0 # via py4dstem -pymatgen==2025.10.7 +pymatgen==2026.3.23 # via # crystal_toolkit (pyproject.toml) # emmet-core @@ -705,9 +708,11 @@ pymatgen==2025.10.7 # mp-api # pymatgen-io-validation # robocrys +pymatgen-core==2026.4.16 + # via pymatgen pymatgen-io-validation==0.1.2 # via emmet-core -pymongo==4.16.0 +pymongo==4.17.0 # via matminer pyparsing==3.3.2 # via @@ -715,7 +720,7 @@ pyparsing==3.3.2 # matplotlib pysocks==1.7.1 # via requests -pytest==9.0.2 +pytest==9.0.3 # via # crystal_toolkit (pyproject.toml) # dash @@ -725,7 +730,7 @@ pytest==9.0.2 # pytest-timeout pytest-base-url==2.1.0 # via pytest-playwright -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via crystal_toolkit (pyproject.toml) pytest-playwright==0.7.2 # via crystal_toolkit (pyproject.toml) @@ -738,11 +743,11 @@ python-dateutil==2.9.0.post0 # jupyter-client # matplotlib # pandas -python-discovery==1.1.3 +python-discovery==1.2.2 # via virtualenv python-dotenv==1.2.2 # via pydantic-settings -python-json-logger==4.0.0 +python-json-logger==4.1.0 # via jupyter-events python-slugify==8.0.4 # via pytest-playwright @@ -770,14 +775,14 @@ pyzmq==27.1.0 # notebook recommonmark==0.7.1 # via crystal_toolkit (pyproject.toml) -redis==7.3.0 +redis==7.4.0 # via crystal_toolkit (pyproject.toml) referencing==0.37.0 # via # jsonschema # jsonschema-specifications # jupyter-events -requests[security,socks]==2.32.5 +requests[security,socks]==2.33.1 # via # dash # dephell @@ -790,7 +795,7 @@ requests[security,socks]==2.32.5 # matminer # mp-api # percy - # pymatgen + # pymatgen-core # pymatgen-io-validation # pytest-base-url # sphinx @@ -818,8 +823,7 @@ ruamel-yaml==0.19.1 # via # dephell # monty - # pymatgen -s3transfer==0.16.0 +s3transfer==0.16.1 # via boto3 scikit-image==0.26.0 # via @@ -848,7 +852,7 @@ scipy==1.17.1 # ncempy # py4dstem # pylops - # pymatgen + # pymatgen-core # robocrys # scikit-image # scikit-learn @@ -867,7 +871,7 @@ shapely==2.1.2 # via crystal_toolkit (pyproject.toml) shellingham==1.5.4 # via dephell-shells -simplejson==3.20.2 +simplejson==4.1.0 # via choreographer six==1.17.0 # via @@ -888,7 +892,7 @@ spglib==2.7.0 # hiphive # ifermi # phonopy - # pymatgen + # pymatgen-core # robocrys # symfc sphinx==9.0.4 @@ -914,17 +918,17 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx stack-data==0.6.3 # via ipython -symfc==1.6.1 +symfc==1.7.0 # via phonopy sympy==1.14.0 # via # hiphive # matminer - # pymatgen + # pymatgen-core tabulate==0.10.0 # via # ifermi - # pymatgen + # pymatgen-core tblib==3.2.2 # via distributed termcolor==3.3.0 @@ -968,7 +972,7 @@ tqdm==4.67.3 # matminer # mpire # py4dstem - # pymatgen + # pymatgen-core trainstation==1.2 # via hiphive traitlets==5.14.3 @@ -984,7 +988,7 @@ traitlets==5.14.3 # nbconvert # nbformat # notebook -trimesh==4.11.3 +trimesh==4.12.0 # via ifermi typeguard==4.5.1 # via inflect @@ -997,6 +1001,7 @@ typing-extensions==4.15.0 # dash # dataclass-wizard # emmet-core + # gdown # ipython # mp-api # pydantic @@ -1011,12 +1016,12 @@ typing-inspection==0.4.2 # via # pydantic # pydantic-settings -tzdata==2025.3 +tzdata==2026.2 # via # arrow # pandas uncertainties==3.2.3 - # via pymatgen + # via pymatgen-core uri-template==1.3.0 # via jsonschema urllib3==1.26.20 @@ -1026,9 +1031,9 @@ urllib3==1.26.20 # distributed # requests # selenium -virtualenv==21.2.0 +virtualenv==21.2.4 # via pre-commit -vtk==9.6.0 +vtk==9.6.1 # via dash-vtk waitress==3.0.2 # via dash @@ -1044,7 +1049,7 @@ webencodings==0.5.1 # tinycss2 websocket-client==1.9.0 # via jupyter-server -werkzeug==3.1.6 +werkzeug==3.1.8 # via # dash # flask @@ -1060,11 +1065,11 @@ ypy-websocket==0.8.4 # via jupyter-server-ydoc zict==3.0.0 # via distributed -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata zope-event==6.1 # via gevent -zope-interface==8.2 +zope-interface==8.3 # via gevent # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/ubuntu-latest_py3.12.txt b/requirements/ubuntu-latest_py3.12.txt index 83446f99..2441ead5 100644 --- a/requirements/ubuntu-latest_py3.12.txt +++ b/requirements/ubuntu-latest_py3.12.txt @@ -9,24 +9,24 @@ annotated-types==0.7.0 asttokens==3.0.1 # via stack-data bibtexparser==1.4.4 - # via pymatgen + # via pymatgen-core blake3==1.0.8 # via emmet-core blinker==1.9.0 # via flask -boto3==1.42.68 +boto3==1.42.95 # via mp-api -botocore==1.42.68 +botocore==1.42.95 # via # boto3 # s3transfer cachelib==0.13.0 # via flask-caching -certifi==2026.2.25 +certifi==2026.4.22 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via flask contourpy==1.3.3 # via matplotlib @@ -34,7 +34,7 @@ cycler==0.12.1 # via matplotlib dash==3.4.0 # via crystal_toolkit (pyproject.toml) -dash-mp-components==0.5.1rc5 +dash-mp-components==0.5.1rc9 # via crystal_toolkit (pyproject.toml) decorator==5.2.1 # via ipython @@ -46,19 +46,19 @@ flask==3.1.3 # via # dash # flask-caching -flask-caching==2.3.1 +flask-caching==2.4.0 # via crystal_toolkit (pyproject.toml) fonttools==4.62.1 # via matplotlib frozendict==2.4.7 # via crystal_toolkit (pyproject.toml) -idna==3.11 +idna==3.13 # via requests imageio==2.37.3 # via scikit-image -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via dash -ipython==9.11.0 +ipython==9.13.0 # via crystal_toolkit (pyproject.toml) ipython-pygments-lexers==1.1.1 # via ipython @@ -74,7 +74,7 @@ jmespath==1.1.0 # botocore joblib==1.5.3 # via - # pymatgen + # pymatgen-core # scikit-learn kiwisolver==1.5.0 # via matplotlib @@ -82,40 +82,42 @@ latexcodec==3.0.1 # via pybtex lazy-loader==0.5 # via scikit-image +lxml==6.1.0 + # via pymatgen-core markupsafe==3.0.3 # via # flask # jinja2 # werkzeug -matplotlib==3.10.8 - # via pymatgen +matplotlib==3.10.9 + # via pymatgen-core matplotlib-inline==0.2.1 # via ipython monty==2026.2.18 # via # emmet-core # mp-api - # pymatgen + # pymatgen-core mp-api==0.46.0 # via crystal_toolkit (pyproject.toml) mpmath==1.3.0 # via sympy -narwhals==2.18.0 +narwhals==2.20.0 # via plotly nest-asyncio==1.6.0 # via dash networkx==3.6.1 # via - # pymatgen + # pymatgen-core # scikit-image -numpy==2.4.3 +numpy==2.4.4 # via # contourpy # imageio # matplotlib # monty # pandas - # pymatgen + # pymatgen-core # pymatgen-io-validation # scikit-image # scikit-learn @@ -123,63 +125,67 @@ numpy==2.4.3 # shapely # spglib # tifffile -orjson==3.11.7 +orjson==3.11.8 # via # mp-api - # pymatgen -packaging==26.0 + # pymatgen-core +packaging==26.1 # via # lazy-loader # matplotlib # plotly # scikit-image palettable==3.3.3 - # via pymatgen -pandas==3.0.1 - # via pymatgen + # via pymatgen-core +pandas==3.0.2 + # via pymatgen-core parso==0.8.6 # via jedi pexpect==4.9.0 # via ipython -pillow==12.1.1 +pillow==12.2.0 # via # imageio # matplotlib # scikit-image -plotly==6.6.0 +plotly==6.7.0 # via # dash - # pymatgen + # pymatgen-core prompt-toolkit==3.0.52 # via ipython +psutil==7.2.2 + # via ipython ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pybtex==0.25.1 +pybtex==0.26.1 # via emmet-core -pydantic==2.12.5 +pydantic==2.13.3 # via # emmet-core # pydantic-settings # pymatgen-io-validation -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic -pydantic-settings==2.13.1 +pydantic-settings==2.14.0 # via # crystal_toolkit (pyproject.toml) # emmet-core # pymatgen-io-validation -pygments==2.19.2 +pygments==2.20.0 # via # ipython # ipython-pygments-lexers -pymatgen==2025.10.7 +pymatgen==2026.3.23 # via # crystal_toolkit (pyproject.toml) # emmet-core # mp-api # pymatgen-io-validation +pymatgen-core==2026.4.16 + # via pymatgen pymatgen-io-validation==0.1.2 # via emmet-core pyparsing==3.3.2 @@ -195,19 +201,17 @@ python-dotenv==1.2.2 # via pydantic-settings pyyaml==6.0.3 # via pybtex -requests==2.32.5 +requests==2.33.1 # via # dash # mp-api - # pymatgen + # pymatgen-core # pymatgen-io-validation retrying==1.4.2 # via dash ruamel-yaml==0.19.1 - # via - # monty - # pymatgen -s3transfer==0.16.0 + # via monty +s3transfer==0.16.1 # via boto3 scikit-image==0.26.0 # via crystal_toolkit (pyproject.toml) @@ -215,7 +219,7 @@ scikit-learn==1.8.0 # via crystal_toolkit (pyproject.toml) scipy==1.17.1 # via - # pymatgen + # pymatgen-core # scikit-image # scikit-learn shapely==2.1.2 @@ -223,19 +227,19 @@ shapely==2.1.2 six==1.17.0 # via python-dateutil spglib==2.7.0 - # via pymatgen + # via pymatgen-core stack-data==0.6.3 # via ipython sympy==1.14.0 - # via pymatgen + # via pymatgen-core tabulate==0.10.0 - # via pymatgen + # via pymatgen-core threadpoolctl==3.6.0 # via scikit-learn -tifffile==2026.3.3 +tifffile==2026.4.11 # via scikit-image tqdm==4.67.3 - # via pymatgen + # via pymatgen-core traitlets==5.14.3 # via # ipython @@ -254,7 +258,7 @@ typing-inspection==0.4.2 # pydantic # pydantic-settings uncertainties==3.2.3 - # via pymatgen + # via pymatgen-core urllib3==2.6.3 # via # botocore @@ -263,11 +267,11 @@ wcwidth==0.6.0 # via prompt-toolkit webcolors==25.10.0 # via crystal_toolkit (pyproject.toml) -werkzeug==3.1.6 +werkzeug==3.1.8 # via # dash # flask -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/ubuntu-latest_py3.12_extras.txt b/requirements/ubuntu-latest_py3.12_extras.txt index 3d28bbf3..5656f5f4 100644 --- a/requirements/ubuntu-latest_py3.12_extras.txt +++ b/requirements/ubuntu-latest_py3.12_extras.txt @@ -8,7 +8,7 @@ aiofiles==22.1.0 # via ypy-websocket aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.3 +aiohttp==3.13.5 # via dephell aiosignal==1.4.0 # via aiohttp @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -anyio==4.12.1 +anyio==4.13.0 # via jupyter-server argon2-cffi==25.1.0 # via @@ -28,14 +28,14 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi arrow==1.4.0 # via isoduration -ase==3.27.0 +ase==3.28.0 # via # boltztrap2 # dscribe # hiphive asttokens==3.0.1 # via stack-data -attrs==25.4.0 +attrs==26.1.0 # via # aiohttp # dephell @@ -59,7 +59,7 @@ beautifulsoup4==4.14.3 # gdown # nbconvert bibtexparser==1.4.4 - # via pymatgen + # via pymatgen-core black==26.3.1 # via crystal_toolkit (pyproject.toml) blake3==1.0.8 @@ -72,9 +72,9 @@ boltztrap2==25.3.1 # via # crystal_toolkit (pyproject.toml) # ifermi -boto3==1.42.68 +boto3==1.42.95 # via mp-api -botocore==1.42.68 +botocore==1.42.95 # via # boto3 # s3transfer @@ -82,7 +82,7 @@ cachelib==0.13.0 # via flask-caching cerberus==1.3.8 # via dephell -certifi==2026.2.25 +certifi==2026.4.22 # via # dephell # netcdf4 @@ -95,11 +95,11 @@ cfgv==3.5.0 # via pre-commit cftime==1.6.5 # via netcdf4 -charset-normalizer==3.4.5 +charset-normalizer==3.4.7 # via requests choreographer==1.2.1 # via kaleido -click==8.3.1 +click==8.3.3 # via # black # dask @@ -118,9 +118,9 @@ commonmark==0.9.1 # via recommonmark contourpy==1.3.3 # via matplotlib -coverage[toml]==7.13.4 +coverage[toml]==7.13.5 # via pytest-cov -cryptography==46.0.5 +cryptography==46.0.7 # via dash crystaltoolkit-extension==0.6.0 # via crystal_toolkit (pyproject.toml) @@ -135,13 +135,13 @@ dash[testing]==3.4.0 # dash-vtk dash-extensions==2.0.5 # via crystal_toolkit (pyproject.toml) -dash-mp-components==0.5.1rc5 +dash-mp-components==0.5.1rc9 # via crystal_toolkit (pyproject.toml) dash-testing-stub==0.0.2 # via dash dash-vtk==0.0.9 # via crystal_toolkit (pyproject.toml) -dask==2026.1.2 +dask==2026.3.0 # via # distributed # py4dstem @@ -192,7 +192,7 @@ dill==0.4.1 # py4dstem distlib==0.4.0 # via virtualenv -distributed==2026.1.2 +distributed==2026.3.0 # via py4dstem dnspython==2.8.0 # via pymongo @@ -216,7 +216,7 @@ executing==2.2.1 # via stack-data fastjsonschema==2.21.2 # via nbformat -filelock==3.25.2 +filelock==3.29.0 # via # gdown # python-discovery @@ -225,7 +225,7 @@ flask==3.1.3 # via # dash # flask-caching -flask-caching==2.3.1 +flask-caching==2.4.0 # via # crystal_toolkit (pyproject.toml) # dash-extensions @@ -239,17 +239,17 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal -fsspec==2026.2.0 +fsspec==2026.3.0 # via dask -gdown==5.2.1 +gdown==6.0.0 # via py4dstem -gevent==25.9.1 +gevent==26.4.0 # via gunicorn -greenlet==3.3.2 +greenlet==3.4.0 # via # gevent # playwright -gunicorn[gevent]==25.1.0 +gunicorn[gevent]==25.3.0 # via crystal_toolkit (pyproject.toml) h5py==3.16.0 # via @@ -265,9 +265,9 @@ hdf5plugin==6.0.0 # via py4dstem hiphive==1.5 # via crystal_toolkit (pyproject.toml) -identify==2.6.17 +identify==2.6.19 # via pre-commit -idna==3.11 +idna==3.13 # via # anyio # jsonschema @@ -279,7 +279,7 @@ imageio==2.37.3 # via scikit-image imagesize==2.0.0 # via sphinx -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via dash inflect==7.5.0 # via robocrys @@ -289,7 +289,7 @@ ipykernel==6.29.5 # via # nbclassic # notebook -ipython==9.11.0 +ipython==9.13.0 # via # crystal_toolkit (pyproject.toml) # ipykernel @@ -325,14 +325,14 @@ jmespath==1.1.0 joblib==1.5.3 # via # dscribe - # pymatgen + # pymatgen-core # scikit-learn # scikit-optimize jsbeautifier==1.15.4 # via dash-extensions -json5==0.13.0 +json5==0.14.0 # via jupyterlab-server -jsonpointer==3.0.0 +jsonpointer==3.1.1 # via jsonschema jsonschema[format-nongpl]==4.26.0 # via @@ -357,7 +357,7 @@ jupyter-core==5.9.1 # nbconvert # nbformat # notebook -jupyter-events==0.12.0 +jupyter-events==0.12.1 # via # jupyter-server # jupyter-server-fileid @@ -393,7 +393,7 @@ latexcodec==3.0.1 # via pybtex lazy-loader==0.5 # via scikit-image -llvmlite==0.46.0 +llvmlite==0.47.0 # via numba locket==1.0.0 # via @@ -403,8 +403,10 @@ logistro==2.0.1 # via # choreographer # kaleido -lxml==6.0.2 - # via dash +lxml==6.1.0 + # via + # dash + # pymatgen-core m2r==0.2.1 # via dephell markupsafe==3.0.3 @@ -413,9 +415,9 @@ markupsafe==3.0.3 # jinja2 # nbconvert # werkzeug -matminer==0.10.0 +matminer==0.10.1 # via robocrys -matplotlib==3.10.8 +matplotlib==3.10.9 # via # ase # boltztrap2 @@ -423,7 +425,7 @@ matplotlib==3.10.8 # ncempy # phonopy # py4dstem - # pymatgen + # pymatgen-core # vtk matplotlib-inline==0.2.1 # via @@ -441,9 +443,9 @@ monty==2026.2.18 # ifermi # matminer # mp-api - # pymatgen + # pymatgen-core # robocrys -more-itertools==10.8.0 +more-itertools==11.0.2 # via # dash-extensions # inflect @@ -463,7 +465,7 @@ multiprocess==0.70.19 # via dash mypy-extensions==1.1.0 # via black -narwhals==2.18.0 +narwhals==2.20.0 # via plotly nbclassic==1.3.3 # via @@ -471,7 +473,7 @@ nbclassic==1.3.3 # notebook nbclient==0.10.4 # via nbconvert -nbconvert==7.17.0 +nbconvert==7.17.1 # via # jupyter-server # notebook @@ -495,7 +497,7 @@ netcdf4==1.7.4 networkx==3.6.1 # via # ifermi - # pymatgen + # pymatgen-core # robocrys # scikit-image nodeenv==1.10.0 @@ -504,7 +506,7 @@ notebook==6.5.7 # via jupyterlab notebook-shim==0.2.4 # via nbclassic -numba==0.64.0 +numba==0.65.1 # via # hiphive # sparse @@ -534,7 +536,7 @@ numpy==1.26.4 # py4dstem # pyfftw # pylops - # pymatgen + # pymatgen-core # pymatgen-io-validation # robocrys # scikit-image @@ -550,12 +552,12 @@ numpy==1.26.4 # trimesh numpy-stl==3.2.0 # via meshcut -orjson==3.11.7 +orjson==3.11.8 # via # kaleido # mp-api - # pymatgen -packaging==26.0 + # pymatgen-core +packaging==26.1 # via # black # dask @@ -581,19 +583,19 @@ packaging==26.0 # scikit-optimize # sphinx palettable==3.3.3 - # via pymatgen + # via pymatgen-core pandas==2.3.3 # via # hiphive # matminer - # pymatgen + # pymatgen-core pandocfilters==1.5.1 # via nbconvert parso==0.8.6 # via jedi partd==1.4.2 # via dask -pathspec==1.0.4 +pathspec==1.1.0 # via black percy==2.0.2 # via dash @@ -601,14 +603,14 @@ pexpect==4.9.0 # via # dephell-shells # ipython -phonopy==3.1.0 +phonopy==3.5.1 # via crystal_toolkit (pyproject.toml) -pillow==12.1.1 +pillow==12.2.0 # via # imageio # matplotlib # scikit-image -platformdirs==4.9.4 +platformdirs==4.9.6 # via # black # jupyter-core @@ -618,18 +620,18 @@ playwright==1.58.0 # via # crystal_toolkit (pyproject.toml) # pytest-playwright -plotly==6.6.0 +plotly==6.7.0 # via # dash # ifermi - # pymatgen + # pymatgen-core pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.5.1 +pre-commit==4.6.0 # via crystal_toolkit (pyproject.toml) -prometheus-client==0.24.1 +prometheus-client==0.25.0 # via # jupyter-server # notebook @@ -645,6 +647,7 @@ psutil==7.2.2 # distributed # ifermi # ipykernel + # ipython ptyprocess==0.7.0 # via # pexpect @@ -657,21 +660,21 @@ py4dstem==0.14.18 # via crystal_toolkit (pyproject.toml) pyaml==26.2.1 # via scikit-optimize -pybtex==0.25.1 +pybtex==0.26.1 # via # emmet-core # robocrys pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via # dash-extensions # emmet-core # pydantic-settings # pymatgen-io-validation -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic -pydantic-settings==2.13.1 +pydantic-settings==2.14.0 # via # crystal_toolkit (pyproject.toml) # emmet-core @@ -682,7 +685,7 @@ pyfftw==0.15.1 # via # crystal_toolkit (pyproject.toml) # ifermi -pygments==2.19.2 +pygments==2.20.0 # via # ipython # ipython-pygments-lexers @@ -692,7 +695,7 @@ pygments==2.19.2 # sphinx pylops==2.6.0 # via py4dstem -pymatgen==2025.10.7 +pymatgen==2026.3.23 # via # crystal_toolkit (pyproject.toml) # emmet-core @@ -701,9 +704,11 @@ pymatgen==2025.10.7 # mp-api # pymatgen-io-validation # robocrys +pymatgen-core==2026.4.16 + # via pymatgen pymatgen-io-validation==0.1.2 # via emmet-core -pymongo==4.16.0 +pymongo==4.17.0 # via matminer pyparsing==3.3.2 # via @@ -711,7 +716,7 @@ pyparsing==3.3.2 # matplotlib pysocks==1.7.1 # via requests -pytest==9.0.2 +pytest==9.0.3 # via # crystal_toolkit (pyproject.toml) # dash @@ -721,7 +726,7 @@ pytest==9.0.2 # pytest-timeout pytest-base-url==2.1.0 # via pytest-playwright -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via crystal_toolkit (pyproject.toml) pytest-playwright==0.7.2 # via crystal_toolkit (pyproject.toml) @@ -734,11 +739,11 @@ python-dateutil==2.9.0.post0 # jupyter-client # matplotlib # pandas -python-discovery==1.1.3 +python-discovery==1.2.2 # via virtualenv python-dotenv==1.2.2 # via pydantic-settings -python-json-logger==4.0.0 +python-json-logger==4.1.0 # via jupyter-events python-slugify==8.0.4 # via pytest-playwright @@ -766,14 +771,14 @@ pyzmq==27.1.0 # notebook recommonmark==0.7.1 # via crystal_toolkit (pyproject.toml) -redis==7.3.0 +redis==7.4.0 # via crystal_toolkit (pyproject.toml) referencing==0.37.0 # via # jsonschema # jsonschema-specifications # jupyter-events -requests[security,socks]==2.32.5 +requests[security,socks]==2.33.1 # via # dash # dephell @@ -786,7 +791,7 @@ requests[security,socks]==2.32.5 # matminer # mp-api # percy - # pymatgen + # pymatgen-core # pymatgen-io-validation # pytest-base-url # sphinx @@ -814,8 +819,7 @@ ruamel-yaml==0.19.1 # via # dephell # monty - # pymatgen -s3transfer==0.16.0 +s3transfer==0.16.1 # via boto3 scikit-image==0.26.0 # via @@ -844,7 +848,7 @@ scipy==1.17.1 # ncempy # py4dstem # pylops - # pymatgen + # pymatgen-core # robocrys # scikit-image # scikit-learn @@ -863,7 +867,7 @@ shapely==2.1.2 # via crystal_toolkit (pyproject.toml) shellingham==1.5.4 # via dephell-shells -simplejson==3.20.2 +simplejson==4.1.0 # via choreographer six==1.17.0 # via @@ -884,7 +888,7 @@ spglib==2.7.0 # hiphive # ifermi # phonopy - # pymatgen + # pymatgen-core # robocrys # symfc sphinx==9.1.0 @@ -910,17 +914,17 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx stack-data==0.6.3 # via ipython -symfc==1.6.1 +symfc==1.7.0 # via phonopy sympy==1.14.0 # via # hiphive # matminer - # pymatgen + # pymatgen-core tabulate==0.10.0 # via # ifermi - # pymatgen + # pymatgen-core tblib==3.2.2 # via distributed termcolor==3.3.0 @@ -964,7 +968,7 @@ tqdm==4.67.3 # matminer # mpire # py4dstem - # pymatgen + # pymatgen-core trainstation==1.2 # via hiphive traitlets==5.14.3 @@ -980,7 +984,7 @@ traitlets==5.14.3 # nbconvert # nbformat # notebook -trimesh==4.11.3 +trimesh==4.12.0 # via ifermi typeguard==4.5.1 # via inflect @@ -1005,12 +1009,12 @@ typing-inspection==0.4.2 # via # pydantic # pydantic-settings -tzdata==2025.3 +tzdata==2026.2 # via # arrow # pandas uncertainties==3.2.3 - # via pymatgen + # via pymatgen-core uri-template==1.3.0 # via jsonschema urllib3==1.26.20 @@ -1020,9 +1024,9 @@ urllib3==1.26.20 # distributed # requests # selenium -virtualenv==21.2.0 +virtualenv==21.2.4 # via pre-commit -vtk==9.6.0 +vtk==9.6.1 # via dash-vtk waitress==3.0.2 # via dash @@ -1038,7 +1042,7 @@ webencodings==0.5.1 # tinycss2 websocket-client==1.9.0 # via jupyter-server -werkzeug==3.1.6 +werkzeug==3.1.8 # via # dash # flask @@ -1054,11 +1058,11 @@ ypy-websocket==0.8.4 # via jupyter-server-ydoc zict==3.0.0 # via distributed -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata zope-event==6.1 # via gevent -zope-interface==8.2 +zope-interface==8.3 # via gevent # The following packages are considered to be unsafe in a requirements file: From 9c215e6bb126588e95485fc3eac07505eb578fea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 20:10:53 +0000 Subject: [PATCH 13/33] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../apps/examples/reverse_pourbaix_example.py | 4 +-- .../components/reverse_pourbaix.py | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py index c6f8393a..edec3bd8 100755 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -13,8 +13,8 @@ from dash import dcc, html import crystal_toolkit.components as ctc -from crystal_toolkit.settings import SETTINGS from crystal_toolkit.components.reverse_pourbaix import ReversePourbaixDiagramComponent +from crystal_toolkit.settings import SETTINGS app = dash.Dash(assets_folder=SETTINGS.ASSETS_PATH) @@ -45,4 +45,4 @@ ctc.register_crystal_toolkit(app=app, layout=layout) if __name__ == "__main__": - app.run(debug=True, port=8050) \ No newline at end of file + app.run(debug=True, port=8050) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index c73ab9c4..7b790e12 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -22,7 +22,6 @@ from dash.dependencies import Component, Input, Output from dash.exceptions import PreventUpdate from frozendict import frozendict - from pymatgen.analysis.pourbaix_diagram import PREFAC from crystal_toolkit.core.mpcomponent import MPComponent @@ -45,6 +44,7 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 + def _resolve_cutoff(value) -> float: """Unwrap a slider value (which may be a list) to a float, falling back to the default cutoff.""" @@ -98,7 +98,10 @@ class ReversePourbaixDiagramComponent(MPComponent): "side": "left", "tickfont": {"size": 16.0}, "ticks": "inside", - "title": {"font": {"color": "#000000", "size": 24.0}, "text": "Potential (V vs. SHE)"}, + "title": { + "font": {"color": "#000000", "size": 24.0}, + "text": "Potential (V vs. SHE)", + }, "type": "linear", "zeroline": False, }, @@ -127,7 +130,7 @@ def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): Current parquet data is computed with solid filter and default ion concentrations. """ - # TODO: in future, it would be nice if user can disable solid filter and + # TODO: in future, it would be nice if user can disable solid filter and # specify ion concentrations. super().__init__(*args, **kwargs) self._stability_df: pd.DataFrame | None = None @@ -179,15 +182,15 @@ def get_heatmap_figure( v_values = heatmap_data["v_values"] grid = heatmap_data["grid"] - cutoff_key = ReversePourbaixDiagramComponent._format_cutoff_key(stability_cutoff) + cutoff_key = ReversePourbaixDiagramComponent._format_cutoff_key( + stability_cutoff + ) lookup: dict[tuple[float, float], int] = { (point["pH"], point["V"]): point["counts"][cutoff_key] for point in grid } - z_matrix = [ - [lookup.get((ph, v), 0) for ph in ph_values] for v in v_values - ] + z_matrix = [[lookup.get((ph, v), 0) for ph in ph_values] for v in v_values] data: list[go.BaseTraceType] = [] @@ -263,7 +266,7 @@ def _sub_layouts(self) -> dict[str, Component]: config={"displayModeBar": False, "displaylogo": False}, ), ], - #style={"minHeight": "500px"}, + # style={"minHeight": "500px"}, id=self.id("graph-panel"), ) @@ -351,8 +354,10 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): ) return dcc.Graph( - id=self.id("heatmap"), # keep stable id so clickData callback can find it + id=self.id( + "heatmap" + ), # keep stable id so clickData callback can find it figure=figure, responsive=True, config={"displayModeBar": False, "displaylogo": False}, - ) \ No newline at end of file + ) From b0bce4f01b8174c474239ad5514aba945d98fe05 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 7 May 2026 13:30:11 -0700 Subject: [PATCH 14/33] fixes fersponding to PR comments --- .../components/reverse_pourbaix.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 7b790e12..33918e76 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -44,22 +44,6 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 - -def _resolve_cutoff(value) -> float: - """Unwrap a slider value (which may be a list) to a float, falling back - to the default cutoff.""" - if isinstance(value, list): - value = value[0] if value else DEFAULT_CUTOFF - if value is None: - return DEFAULT_CUTOFF - return float(value) - - -def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: - """Snap a clicked (pH, V) point to the precomputed grid keys.""" - return int(round(ph)), round(v * 2) / 2 - - class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -144,6 +128,22 @@ def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): len(df), self._stability_df.index.nunique(), ) + + @staticmethod + def _resolve_cutoff(value: float | list[float] | None) -> float: + """Unwrap a slider value (which may be a list) to a float, falling back + to the default cutoff.""" + if isinstance(value, list): + value = value[0] if value else DEFAULT_CUTOFF + if value is None: + return DEFAULT_CUTOFF + return float(value) + + @staticmethod + def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: + """Snap a clicked (pH, V) point to the precomputed grid keys.""" + return int(round(ph)), round(v * 2) / 2 + @staticmethod def _format_cutoff_key(cutoff: float) -> str: @@ -160,7 +160,7 @@ def get_stable_mp_ids(self, ph: float, v: float, cutoff: float) -> list[str]: """ if self._stability_df is None: return [] - ph_key, v_key = _snap_to_grid(ph, v) + ph_key, v_key = self._snap_to_grid(ph, v) try: cell = self._stability_df.loc[(ph_key, v_key)] except KeyError: @@ -333,7 +333,7 @@ def generate_callbacks(self, app, cache) -> None: """Register Dash callbacks for interactivity.""" @app.callback( - Output(self.id("graph-panel"), "children"), + Output(self.id("heatmap"), "figure"), Input(self.id(), "data"), Input(self.get_kwarg_id("show_water_lines"), "value"), Input(self.get_kwarg_id("stability_cutoff"), "value"), @@ -349,15 +349,8 @@ def update_figure(heatmap_json, show_water_lines, stability_cutoff): figure = self.get_heatmap_figure( heatmap_data, - stability_cutoff=_resolve_cutoff(stability_cutoff), + stability_cutoff=self._resolve_cutoff(stability_cutoff), show_water_lines=bool(show_water_lines), ) - return dcc.Graph( - id=self.id( - "heatmap" - ), # keep stable id so clickData callback can find it - figure=figure, - responsive=True, - config={"displayModeBar": False, "displaylogo": False}, - ) + return figure From 6c43a1b5b15c135e90a919ad27db23a3b86a2106 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 20:30:37 +0000 Subject: [PATCH 15/33] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- crystal_toolkit/components/reverse_pourbaix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 33918e76..caaa6f27 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -44,6 +44,7 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 + class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -128,7 +129,7 @@ def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): len(df), self._stability_df.index.nunique(), ) - + @staticmethod def _resolve_cutoff(value: float | list[float] | None) -> float: """Unwrap a slider value (which may be a list) to a float, falling back @@ -143,7 +144,6 @@ def _resolve_cutoff(value: float | list[float] | None) -> float: def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: """Snap a clicked (pH, V) point to the precomputed grid keys.""" return int(round(ph)), round(v * 2) / 2 - @staticmethod def _format_cutoff_key(cutoff: float) -> str: From b81e001848be190b8cdf45053a6106efa9907918 Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 20 May 2026 15:41:54 -0700 Subject: [PATCH 16/33] test with dummy options for solid filter and ion conc. --- .../components/reverse_pourbaix.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index caaa6f27..9c25a798 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -44,6 +44,12 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 +# Ion concentration options (mol/L) for the dummy dropdown. Materials Project +# default is 1e-6 M; the others span the practical range used in literature. +ION_CONCENTRATION_OPTIONS = [1e-8, 1e-6, 1e-4, 1e-2, 1.0] +DEFAULT_ION_CONCENTRATION = 1e-6 + +DEFAULT_FILTER_SOLIDS = True class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -56,6 +62,8 @@ class ReversePourbaixDiagramComponent(MPComponent): default_state = frozendict( show_water_lines=True, stability_cutoff=DEFAULT_CUTOFF, + filter_solids=DEFAULT_FILTER_SOLIDS, + ion_concentration=DEFAULT_ION_CONCENTRATION, ) default_plot_style = frozendict( @@ -313,6 +321,50 @@ def _sub_layouts(self) -> dict[str, Component]: "Higher cutoffs include progressively more metastable phases." ), ), + self.get_bool_input( + kwarg_label="filter_solids", + default=self.default_state["filter_solids"], + label="Filter Solid Phases", + help_str=( + "When enabled, only solid phases that are stable on the " + "compositional phase diagram are included in the Pourbaix " + "analysis. Highly oxidized or reduced phases that may appear " + "in experiments due to kinetic limitations on oxygen or " + "hydrogen evolution are excluded — these are not truly " + "thermodynamically stable and are often overstabilized by " + "DFT errors. Including only stable solid phases generally " + "yields the most accurate Pourbaix diagrams. " + "(Currently a placeholder — does not affect the diagram.)" + ), + ), + html.Div( + [ + html.Label( + "Ion Concentration (mol/L)", + className="mpc-label", + style={"fontWeight": "bold"}, + ), + dcc.Dropdown( + id=self.get_kwarg_id("ion_concentration"), + options=[ + {"label": f"{c:.0e} M", "value": c} + for c in ION_CONCENTRATION_OPTIONS + ], + value=self.default_state["ion_concentration"], + clearable=False, + style={"maxWidth": "240px"}, + ), + html.P( + "Activity of dissolved ionic species used to construct the " + "Pourbaix diagram. Lower concentrations expand the stability " + "regions of solid phases. (Currently a placeholder — does " + "not affect the diagram.)", + className="mpc-help", + style={"fontSize": "0.85em", "marginTop": "0.25rem"}, + ), + ], + style={"marginTop": "1rem"}, + ), ] ) @@ -338,7 +390,7 @@ def generate_callbacks(self, app, cache) -> None: Input(self.get_kwarg_id("show_water_lines"), "value"), Input(self.get_kwarg_id("stability_cutoff"), "value"), ) - def update_figure(heatmap_json, show_water_lines, stability_cutoff): + def update_figure(heatmap_json, show_water_lines, stability_cutoff): # some options ignored for now if not heatmap_json: raise PreventUpdate From ca2ea36edf427e8b20d5b061b83141aa41599887 Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 20 May 2026 15:58:54 -0700 Subject: [PATCH 17/33] test with dummy options for solid filter and ion conc. --- .../components/reverse_pourbaix.py | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 9c25a798..4a9e8701 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -337,33 +337,20 @@ def _sub_layouts(self) -> dict[str, Component]: "(Currently a placeholder — does not affect the diagram.)" ), ), - html.Div( - [ - html.Label( - "Ion Concentration (mol/L)", - className="mpc-label", - style={"fontWeight": "bold"}, - ), - dcc.Dropdown( - id=self.get_kwarg_id("ion_concentration"), - options=[ - {"label": f"{c:.0e} M", "value": c} - for c in ION_CONCENTRATION_OPTIONS - ], - value=self.default_state["ion_concentration"], - clearable=False, - style={"maxWidth": "240px"}, - ), - html.P( - "Activity of dissolved ionic species used to construct the " - "Pourbaix diagram. Lower concentrations expand the stability " - "regions of solid phases. (Currently a placeholder — does " - "not affect the diagram.)", - className="mpc-help", - style={"fontSize": "0.85em", "marginTop": "0.25rem"}, - ), + self.get_choice_input( + kwarg_label="ion_concentration", + default=self.default_state["ion_concentration"], + options=[ + {"label": f"{c:.0e} M", "value": c} + for c in ION_CONCENTRATION_OPTIONS ], - style={"marginTop": "1rem"}, + label="Ion Concentration (mol/L)", + help_str=( + "Activity of dissolved ionic species used to construct the " + "Pourbaix diagram, applied uniformly to all elements. Lower " + "concentrations expand the stability regions of solid phases. " + "(Currently a placeholder — does not affect the diagram.)" + ), ), ] ) From ce9a150dbf72449423f5fb6f21bea5cc754a8ccc Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 22 May 2026 11:30:30 -0700 Subject: [PATCH 18/33] functionality to allow acceptance of composition from reverse_pourbaix build link --- crystal_toolkit/components/pourbaix.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index 64cc652f..496b8ac8 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -683,10 +683,15 @@ def update_heatmap_choices(entries, mat_detials, filter_solids): Output(self.id("comp-conc-btn"), "children"), Output(self.id("comp-conc-btn"), "style"), Input(self.id(), "data"), + # Optional: prefilled ratio from a parent app linking via + # `links={"prefill-ratio": parent_app.id("prefill-ratio")}`. + # If not linked, falls back to equal ratios. + State(self.id("prefill-ratio"), "data"), prevent_initial_call=True, ) def update_element_specific_sliders( entries, + prefill_ratio, ): """ When pourbaix entries input, add concentration and composition options @@ -706,7 +711,7 @@ def update_element_specific_sliders( conc_inputs = [] - for element in sorted(elements): + for element in sorted(elements, key=lambda e: e.symbol): conc_input = PourbaixDiagramComponent.create_centered_object( self.get_numerical_input( f"conc-{element}", @@ -755,10 +760,19 @@ def update_element_specific_sliders( } # elements store - elements = [element.symbol for element in elements] - - # default_comp - default_comp = ":".join(["1" for _ in elements]) + # Sort alphabetically for deterministic ordering across page loads. + # This order is the contract between the composition title display, + # the comp-text positional parsing in make_figure, and any URL-based + # ratio prefill from the reverse-Pourbaix app. + elements = sorted(element.symbol for element in elements) + + # default_comp — use prefilled ratio from URL if supplied and length + # matches the resolved element list, otherwise fall back to all-equal. + if prefill_ratio and len(prefill_ratio) == len(elements): + default_comp = ":".join(str(int(r)) if r == int(r) else str(r) + for r in prefill_ratio) + else: + default_comp = ":".join(["1" for _ in elements]) # composition title title = "💡 Composition of " + ":".join(elements) From ec4dbdfcbd05433db85ebb2b8bbf67b6c3753d21 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 18:38:10 +0000 Subject: [PATCH 19/33] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- crystal_toolkit/components/pourbaix.py | 5 +++-- crystal_toolkit/components/reverse_pourbaix.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index 496b8ac8..0c3d0f13 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -769,8 +769,9 @@ def update_element_specific_sliders( # default_comp — use prefilled ratio from URL if supplied and length # matches the resolved element list, otherwise fall back to all-equal. if prefill_ratio and len(prefill_ratio) == len(elements): - default_comp = ":".join(str(int(r)) if r == int(r) else str(r) - for r in prefill_ratio) + default_comp = ":".join( + str(int(r)) if r == int(r) else str(r) for r in prefill_ratio + ) else: default_comp = ":".join(["1" for _ in elements]) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 4a9e8701..c0e82bd7 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -51,6 +51,7 @@ DEFAULT_FILTER_SOLIDS = True + class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -377,7 +378,9 @@ def generate_callbacks(self, app, cache) -> None: Input(self.get_kwarg_id("show_water_lines"), "value"), Input(self.get_kwarg_id("stability_cutoff"), "value"), ) - def update_figure(heatmap_json, show_water_lines, stability_cutoff): # some options ignored for now + def update_figure( + heatmap_json, show_water_lines, stability_cutoff + ): # some options ignored for now if not heatmap_json: raise PreventUpdate From abb9c3f7a7020d903076577d7a949f07919c31aa Mon Sep 17 00:00:00 2001 From: zccalka Date: Fri, 22 May 2026 12:24:53 -0700 Subject: [PATCH 20/33] updated json for example --- .../examples/reverse_pourbaix_heatmap.json | 1374 ++++++++--------- 1 file changed, 687 insertions(+), 687 deletions(-) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json index 423330a7..3c0a8ffb 100644 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json @@ -1,20 +1,20 @@ { "ph_values": [ - 0.0, - 1.0, - 2.0, - 3.0, - 4.0, - 5.0, - 6.0, - 7.0, - 8.0, - 9.0, - 10.0, - 11.0, - 12.0, - 13.0, - 14.0 + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 ], "v_values": [ 2.0, @@ -39,44 +39,44 @@ "pH": 0.0, "V": -2.0, "counts": { - "0.1": 4434, - "0.2": 5621, - "0.3": 6819, - "0.4": 8044, - "0.5": 9464 + "0.1": 4435, + "0.2": 5622, + "0.3": 6820, + "0.4": 8045, + "0.5": 9467 } }, { "pH": 0.0, "V": -1.5, "counts": { - "0.1": 3824, - "0.2": 5137, - "0.3": 6644, - "0.4": 8830, - "0.5": 11695 + "0.1": 3825, + "0.2": 5138, + "0.3": 6649, + "0.4": 8839, + "0.5": 11708 } }, { "pH": 0.0, "V": -1.0, "counts": { - "0.1": 3039, - "0.2": 4665, - "0.3": 7275, - "0.4": 11410, - "0.5": 17082 + "0.1": 3040, + "0.2": 4670, + "0.3": 7284, + "0.4": 11428, + "0.5": 17109 } }, { "pH": 0.0, "V": -0.5, "counts": { - "0.1": 2325, - "0.2": 4948, - "0.3": 9897, - "0.4": 16961, - "0.5": 26452 + "0.1": 2326, + "0.2": 4953, + "0.3": 9906, + "0.4": 16986, + "0.5": 26486 } }, { @@ -84,10 +84,10 @@ "V": 0.0, "counts": { "0.1": 2070, - "0.2": 7209, - "0.3": 18500, - "0.4": 31128, - "0.5": 43152 + "0.2": 7227, + "0.3": 18532, + "0.4": 31182, + "0.5": 43223 } }, { @@ -95,21 +95,21 @@ "V": 0.5, "counts": { "0.1": 1770, - "0.2": 8887, - "0.3": 22572, - "0.4": 35679, - "0.5": 45744 + "0.2": 8917, + "0.3": 22613, + "0.4": 35745, + "0.5": 45824 } }, { "pH": 0.0, "V": 1.0, "counts": { - "0.1": 2093, - "0.2": 9633, - "0.3": 23973, - "0.4": 37177, - "0.5": 47804 + "0.1": 2094, + "0.2": 9665, + "0.3": 24016, + "0.4": 37245, + "0.5": 47887 } }, { @@ -117,10 +117,10 @@ "V": 1.5, "counts": { "0.1": 2070, - "0.2": 9465, - "0.3": 22426, - "0.4": 34763, - "0.5": 46155 + "0.2": 9492, + "0.3": 22465, + "0.4": 34818, + "0.5": 46223 } }, { @@ -128,1397 +128,1397 @@ "V": 2.0, "counts": { "0.1": 2382, - "0.2": 8651, - "0.3": 19244, - "0.4": 28641, - "0.5": 37909 + "0.2": 8672, + "0.3": 19276, + "0.4": 28691, + "0.5": 37967 } }, { "pH": 1.0, "V": -2.0, "counts": { - "0.1": 4444, - "0.2": 5654, - "0.3": 6941, - "0.4": 8188, - "0.5": 9710 + "0.1": 4445, + "0.2": 5655, + "0.3": 6942, + "0.4": 8189, + "0.5": 9713 } }, { "pH": 1.0, "V": -1.5, "counts": { - "0.1": 3856, - "0.2": 5278, - "0.3": 6967, - "0.4": 9410, - "0.5": 12625 + "0.1": 3857, + "0.2": 5279, + "0.3": 6973, + "0.4": 9419, + "0.5": 12638 } }, { "pH": 1.0, "V": -1.0, "counts": { - "0.1": 3011, - "0.2": 4928, - "0.3": 8041, - "0.4": 12735, - "0.5": 19038 + "0.1": 3012, + "0.2": 4935, + "0.3": 8051, + "0.4": 12753, + "0.5": 19065 } }, { "pH": 1.0, "V": -0.5, "counts": { - "0.1": 2616, - "0.2": 6011, - "0.3": 12098, - "0.4": 21184, - "0.5": 32751 + "0.1": 2617, + "0.2": 6018, + "0.3": 12110, + "0.4": 21213, + "0.5": 32793 } }, { "pH": 1.0, "V": 0.0, "counts": { - "0.1": 2457, - "0.2": 9155, - "0.3": 22229, - "0.4": 36190, - "0.5": 47890 + "0.1": 2458, + "0.2": 9179, + "0.3": 22267, + "0.4": 36252, + "0.5": 47966 } }, { "pH": 1.0, "V": 0.5, "counts": { - "0.1": 2489, - "0.2": 11351, - "0.3": 26402, - "0.4": 38536, - "0.5": 48721 + "0.1": 2490, + "0.2": 11383, + "0.3": 26450, + "0.4": 38609, + "0.5": 48801 } }, { "pH": 1.0, "V": 1.0, "counts": { - "0.1": 2810, - "0.2": 11963, - "0.3": 27730, - "0.4": 40567, - "0.5": 52057 + "0.1": 2814, + "0.2": 11998, + "0.3": 27781, + "0.4": 40641, + "0.5": 52143 } }, { "pH": 1.0, "V": 1.5, "counts": { - "0.1": 2728, - "0.2": 11736, - "0.3": 24895, - "0.4": 37581, - "0.5": 47677 + "0.1": 2729, + "0.2": 11765, + "0.3": 24939, + "0.4": 37641, + "0.5": 47745 } }, { "pH": 1.0, "V": 2.0, "counts": { - "0.1": 2949, - "0.2": 9952, - "0.3": 20382, - "0.4": 29538, - "0.5": 38398 + "0.1": 2950, + "0.2": 9973, + "0.3": 20418, + "0.4": 29592, + "0.5": 38457 } }, { "pH": 2.0, "V": -2.0, "counts": { - "0.1": 4448, - "0.2": 5714, - "0.3": 7049, - "0.4": 8366, - "0.5": 9998 + "0.1": 4449, + "0.2": 5715, + "0.3": 7050, + "0.4": 8368, + "0.5": 10004 } }, { "pH": 2.0, "V": -1.5, "counts": { - "0.1": 3901, - "0.2": 5498, - "0.3": 7448, - "0.4": 10201, - "0.5": 13802 + "0.1": 3902, + "0.2": 5503, + "0.3": 7457, + "0.4": 10210, + "0.5": 13817 } }, { "pH": 2.0, "V": -1.0, "counts": { - "0.1": 3041, - "0.2": 5345, - "0.3": 9127, - "0.4": 14563, - "0.5": 21440 + "0.1": 3044, + "0.2": 5353, + "0.3": 9139, + "0.4": 14581, + "0.5": 21471 } }, { "pH": 2.0, "V": -0.5, "counts": { - "0.1": 3065, - "0.2": 7315, - "0.3": 15525, - "0.4": 27192, - "0.5": 38176 + "0.1": 3068, + "0.2": 7324, + "0.3": 15543, + "0.4": 27227, + "0.5": 38241 } }, { "pH": 2.0, "V": 0.0, "counts": { - "0.1": 2788, - "0.2": 11462, - "0.3": 26986, - "0.4": 40966, - "0.5": 51160 + "0.1": 2793, + "0.2": 11489, + "0.3": 27036, + "0.4": 41037, + "0.5": 51239 } }, { "pH": 2.0, "V": 0.5, "counts": { - "0.1": 3396, - "0.2": 14374, - "0.3": 29943, - "0.4": 41860, - "0.5": 51693 + "0.1": 3405, + "0.2": 14409, + "0.3": 30003, + "0.4": 41935, + "0.5": 51779 } }, { "pH": 2.0, "V": 1.0, "counts": { - "0.1": 3514, - "0.2": 14728, - "0.3": 30979, - "0.4": 43740, - "0.5": 54564 + "0.1": 3522, + "0.2": 14764, + "0.3": 31037, + "0.4": 43820, + "0.5": 54652 } }, { "pH": 2.0, "V": 1.5, "counts": { - "0.1": 3440, - "0.2": 13291, - "0.3": 26116, - "0.4": 38464, - "0.5": 48167 + "0.1": 3446, + "0.2": 13321, + "0.3": 26166, + "0.4": 38525, + "0.5": 48237 } }, { "pH": 2.0, "V": 2.0, "counts": { - "0.1": 3193, - "0.2": 10378, - "0.3": 20903, - "0.4": 29631, - "0.5": 38169 + "0.1": 3196, + "0.2": 10399, + "0.3": 20945, + "0.4": 29685, + "0.5": 38227 } }, { "pH": 3.0, "V": -2.0, "counts": { - "0.1": 4457, - "0.2": 5788, - "0.3": 7164, - "0.4": 8549, - "0.5": 10412 + "0.1": 4458, + "0.2": 5789, + "0.3": 7165, + "0.4": 8551, + "0.5": 10418 } }, { "pH": 3.0, "V": -1.5, "counts": { - "0.1": 3999, - "0.2": 5801, - "0.3": 8031, - "0.4": 11390, - "0.5": 15041 + "0.1": 4000, + "0.2": 5807, + "0.3": 8040, + "0.4": 11399, + "0.5": 15059 } }, { "pH": 3.0, "V": -1.0, "counts": { - "0.1": 3166, - "0.2": 5941, - "0.3": 10468, - "0.4": 16483, - "0.5": 23578 + "0.1": 3171, + "0.2": 5950, + "0.3": 10480, + "0.4": 16502, + "0.5": 23614 } }, { "pH": 3.0, "V": -0.5, "counts": { - "0.1": 3752, - "0.2": 10017, - "0.3": 21299, - "0.4": 32549, - "0.5": 43230 + "0.1": 3757, + "0.2": 10028, + "0.3": 21323, + "0.4": 32598, + "0.5": 43305 } }, { "pH": 3.0, "V": 0.0, "counts": { - "0.1": 3402, - "0.2": 15000, - "0.3": 32180, - "0.4": 44577, - "0.5": 54304 + "0.1": 3413, + "0.2": 15033, + "0.3": 32234, + "0.4": 44650, + "0.5": 54388 } }, { "pH": 3.0, "V": 0.5, "counts": { - "0.1": 4414, - "0.2": 17534, - "0.3": 32935, - "0.4": 45167, - "0.5": 55494 + "0.1": 4430, + "0.2": 17573, + "0.3": 32999, + "0.4": 45247, + "0.5": 55586 } }, { "pH": 3.0, "V": 1.0, "counts": { - "0.1": 4273, - "0.2": 17440, - "0.3": 33527, - "0.4": 46955, - "0.5": 56300 + "0.1": 4287, + "0.2": 17480, + "0.3": 33589, + "0.4": 47038, + "0.5": 56391 } }, { "pH": 3.0, "V": 1.5, "counts": { - "0.1": 4089, - "0.2": 14434, - "0.3": 26821, - "0.4": 38658, - "0.5": 48094 + "0.1": 4102, + "0.2": 14468, + "0.3": 26877, + "0.4": 38718, + "0.5": 48165 } }, { "pH": 3.0, "V": 2.0, "counts": { - "0.1": 3356, - "0.2": 10538, - "0.3": 20461, - "0.4": 28871, - "0.5": 36950 + "0.1": 3358, + "0.2": 10560, + "0.3": 20505, + "0.4": 28922, + "0.5": 37011 } }, { "pH": 4.0, "V": -2.0, "counts": { - "0.1": 4473, - "0.2": 5853, - "0.3": 7252, - "0.4": 8826, - "0.5": 10748 + "0.1": 4474, + "0.2": 5854, + "0.3": 7253, + "0.4": 8830, + "0.5": 10754 } }, { "pH": 4.0, "V": -1.5, "counts": { - "0.1": 4107, - "0.2": 6128, - "0.3": 8876, - "0.4": 12383, - "0.5": 16490 + "0.1": 4108, + "0.2": 6137, + "0.3": 8885, + "0.4": 12396, + "0.5": 16508 } }, { "pH": 4.0, "V": -1.0, "counts": { - "0.1": 3331, - "0.2": 6781, - "0.3": 11893, - "0.4": 18219, - "0.5": 25547 + "0.1": 3336, + "0.2": 6793, + "0.3": 11905, + "0.4": 18241, + "0.5": 25588 } }, { "pH": 4.0, "V": -0.5, "counts": { - "0.1": 4373, - "0.2": 12899, - "0.3": 25038, - "0.4": 36203, - "0.5": 47186 + "0.1": 4379, + "0.2": 12916, + "0.3": 25073, + "0.4": 36267, + "0.5": 47266 } }, { "pH": 4.0, "V": 0.0, "counts": { - "0.1": 4417, - "0.2": 18849, - "0.3": 35651, - "0.4": 47418, - "0.5": 57138 + "0.1": 4438, + "0.2": 18891, + "0.3": 35714, + "0.4": 47495, + "0.5": 57225 } }, { "pH": 4.0, "V": 0.5, "counts": { - "0.1": 5191, - "0.2": 19302, - "0.3": 35387, - "0.4": 47680, - "0.5": 58432 + "0.1": 5218, + "0.2": 19351, + "0.3": 35454, + "0.4": 47762, + "0.5": 58526 } }, { "pH": 4.0, "V": 1.0, "counts": { - "0.1": 4920, - "0.2": 18614, - "0.3": 34981, - "0.4": 47806, - "0.5": 56958 + "0.1": 4944, + "0.2": 18659, + "0.3": 35045, + "0.4": 47885, + "0.5": 57050 } }, { "pH": 4.0, "V": 1.5, "counts": { - "0.1": 4478, - "0.2": 15282, - "0.3": 27222, - "0.4": 38435, - "0.5": 47260 + "0.1": 4495, + "0.2": 15322, + "0.3": 27279, + "0.4": 38495, + "0.5": 47331 } }, { "pH": 4.0, "V": 2.0, "counts": { - "0.1": 3811, - "0.2": 10773, - "0.3": 20317, - "0.4": 27828, - "0.5": 35626 + "0.1": 3814, + "0.2": 10801, + "0.3": 20358, + "0.4": 27877, + "0.5": 35687 } }, { "pH": 5.0, "V": -2.0, "counts": { - "0.1": 4481, - "0.2": 5907, - "0.3": 7381, - "0.4": 9102, - "0.5": 11139 + "0.1": 4482, + "0.2": 5908, + "0.3": 7382, + "0.4": 9107, + "0.5": 11145 } }, { "pH": 5.0, "V": -1.5, "counts": { - "0.1": 4268, - "0.2": 6612, - "0.3": 9683, - "0.4": 13612, - "0.5": 17957 + "0.1": 4270, + "0.2": 6621, + "0.3": 9696, + "0.4": 13626, + "0.5": 17977 } }, { "pH": 5.0, "V": -1.0, "counts": { - "0.1": 3691, - "0.2": 7765, - "0.3": 13204, - "0.4": 19764, - "0.5": 27770 + "0.1": 3697, + "0.2": 7777, + "0.3": 13219, + "0.4": 19789, + "0.5": 27819 } }, { "pH": 5.0, "V": -0.5, "counts": { - "0.1": 4971, - "0.2": 15318, - "0.3": 28201, - "0.4": 40012, - "0.5": 50513 + "0.1": 4979, + "0.2": 15348, + "0.3": 28245, + "0.4": 40084, + "0.5": 50601 } }, { "pH": 5.0, "V": 0.0, "counts": { - "0.1": 5396, - "0.2": 21107, - "0.3": 37210, - "0.4": 48797, - "0.5": 58902 + "0.1": 5422, + "0.2": 21159, + "0.3": 37274, + "0.4": 48880, + "0.5": 58995 } }, { "pH": 5.0, "V": 0.5, "counts": { - "0.1": 5912, - "0.2": 20599, - "0.3": 37490, - "0.4": 50965, - "0.5": 60314 + "0.1": 5944, + "0.2": 20659, + "0.3": 37558, + "0.4": 51055, + "0.5": 60410 } }, { "pH": 5.0, "V": 1.0, "counts": { - "0.1": 5427, - "0.2": 19246, - "0.3": 35257, - "0.4": 48041, - "0.5": 56646 + "0.1": 5455, + "0.2": 19298, + "0.3": 35325, + "0.4": 48118, + "0.5": 56731 } }, { "pH": 5.0, "V": 1.5, "counts": { - "0.1": 4947, - "0.2": 16042, - "0.3": 27544, - "0.4": 38033, - "0.5": 46617 + "0.1": 4967, + "0.2": 16089, + "0.3": 27600, + "0.4": 38097, + "0.5": 46690 } }, { "pH": 5.0, "V": 2.0, "counts": { - "0.1": 4251, - "0.2": 10958, - "0.3": 19878, - "0.4": 26876, - "0.5": 34206 + "0.1": 4256, + "0.2": 10986, + "0.3": 19916, + "0.4": 26924, + "0.5": 34267 } }, { "pH": 6.0, "V": -2.0, "counts": { - "0.1": 4496, - "0.2": 5986, - "0.3": 7579, - "0.4": 9418, - "0.5": 11585 + "0.1": 4497, + "0.2": 5987, + "0.3": 7580, + "0.4": 9424, + "0.5": 11591 } }, { "pH": 6.0, "V": -1.5, "counts": { - "0.1": 4367, - "0.2": 7195, - "0.3": 10575, - "0.4": 14757, - "0.5": 19609 + "0.1": 4371, + "0.2": 7208, + "0.3": 10588, + "0.4": 14773, + "0.5": 19632 } }, { "pH": 6.0, "V": -1.0, "counts": { - "0.1": 3923, - "0.2": 8525, - "0.3": 14283, - "0.4": 21201, - "0.5": 30302 + "0.1": 3931, + "0.2": 8539, + "0.3": 14301, + "0.4": 21228, + "0.5": 30354 } }, { "pH": 6.0, "V": -0.5, "counts": { - "0.1": 5539, - "0.2": 17405, - "0.3": 31174, - "0.4": 43205, - "0.5": 53913 + "0.1": 5553, + "0.2": 17447, + "0.3": 31222, + "0.4": 43284, + "0.5": 54003 } }, { "pH": 6.0, "V": 0.0, "counts": { - "0.1": 6474, - "0.2": 23257, - "0.3": 38880, - "0.4": 50492, - "0.5": 61779 + "0.1": 6508, + "0.2": 23319, + "0.3": 38946, + "0.4": 50580, + "0.5": 61877 } }, { "pH": 6.0, "V": 0.5, "counts": { - "0.1": 6573, - "0.2": 22273, - "0.3": 39682, - "0.4": 52941, - "0.5": 61782 + "0.1": 6610, + "0.2": 22337, + "0.3": 39754, + "0.4": 53032, + "0.5": 61879 } }, { "pH": 6.0, "V": 1.0, "counts": { - "0.1": 5676, - "0.2": 19627, - "0.3": 35338, - "0.4": 47839, - "0.5": 56304 + "0.1": 5709, + "0.2": 19679, + "0.3": 35405, + "0.4": 47916, + "0.5": 56385 } }, { "pH": 6.0, "V": 1.5, "counts": { - "0.1": 5384, - "0.2": 16855, - "0.3": 27837, - "0.4": 37295, - "0.5": 45879 + "0.1": 5409, + "0.2": 16905, + "0.3": 27893, + "0.4": 37360, + "0.5": 45952 } }, { "pH": 6.0, "V": 2.0, "counts": { - "0.1": 4641, - "0.2": 11107, - "0.3": 19189, - "0.4": 25878, - "0.5": 32723 + "0.1": 4650, + "0.2": 11128, + "0.3": 19227, + "0.4": 25921, + "0.5": 32778 } }, { "pH": 7.0, "V": -2.0, "counts": { - "0.1": 4499, - "0.2": 6109, - "0.3": 7756, - "0.4": 9779, - "0.5": 12261 + "0.1": 4500, + "0.2": 6110, + "0.3": 7758, + "0.4": 9785, + "0.5": 12267 } }, { "pH": 7.0, "V": -1.5, "counts": { - "0.1": 4666, - "0.2": 7774, - "0.3": 11546, - "0.4": 16131, - "0.5": 22026 + "0.1": 4675, + "0.2": 7787, + "0.3": 11559, + "0.4": 16150, + "0.5": 22053 } }, { "pH": 7.0, "V": -1.0, "counts": { - "0.1": 4131, - "0.2": 9019, - "0.3": 15362, - "0.4": 23215, - "0.5": 34101 + "0.1": 4140, + "0.2": 9037, + "0.3": 15382, + "0.4": 23249, + "0.5": 34160 } }, { "pH": 7.0, "V": -0.5, "counts": { - "0.1": 6144, - "0.2": 19193, - "0.3": 33651, - "0.4": 45864, - "0.5": 57640 + "0.1": 6164, + "0.2": 19239, + "0.3": 33709, + "0.4": 45948, + "0.5": 57734 } }, { "pH": 7.0, "V": 0.0, "counts": { - "0.1": 7303, - "0.2": 24199, - "0.3": 40524, - "0.4": 53152, - "0.5": 63607 + "0.1": 7341, + "0.2": 24262, + "0.3": 40594, + "0.4": 53246, + "0.5": 63707 } }, { "pH": 7.0, "V": 0.5, "counts": { - "0.1": 7285, - "0.2": 23849, - "0.3": 42204, - "0.4": 54134, - "0.5": 62400 + "0.1": 7327, + "0.2": 23914, + "0.3": 42281, + "0.4": 54227, + "0.5": 62497 } }, { "pH": 7.0, "V": 1.0, "counts": { - "0.1": 5902, - "0.2": 19737, - "0.3": 35434, - "0.4": 47270, - "0.5": 55896 + "0.1": 5937, + "0.2": 19791, + "0.3": 35502, + "0.4": 47349, + "0.5": 55979 } }, { "pH": 7.0, "V": 1.5, "counts": { - "0.1": 5250, - "0.2": 16200, - "0.3": 26836, - "0.4": 35836, - "0.5": 44649 + "0.1": 5273, + "0.2": 16246, + "0.3": 26893, + "0.4": 35899, + "0.5": 44720 } }, { "pH": 7.0, "V": 2.0, "counts": { - "0.1": 5008, - "0.2": 11205, - "0.3": 18593, - "0.4": 24962, - "0.5": 31291 + "0.1": 5017, + "0.2": 11220, + "0.3": 18630, + "0.4": 25004, + "0.5": 31343 } }, { "pH": 8.0, "V": -2.0, "counts": { - "0.1": 4539, - "0.2": 6246, - "0.3": 7995, - "0.4": 10188, - "0.5": 13197 + "0.1": 4540, + "0.2": 6247, + "0.3": 7997, + "0.4": 10194, + "0.5": 13205 } }, { "pH": 8.0, "V": -1.5, "counts": { - "0.1": 4926, - "0.2": 8407, - "0.3": 12667, - "0.4": 17896, - "0.5": 24829 + "0.1": 4936, + "0.2": 8420, + "0.3": 12682, + "0.4": 17916, + "0.5": 24867 } }, { "pH": 8.0, "V": -1.0, "counts": { - "0.1": 4467, - "0.2": 9767, - "0.3": 16868, - "0.4": 26875, - "0.5": 39381 + "0.1": 4478, + "0.2": 9786, + "0.3": 16889, + "0.4": 26920, + "0.5": 39457 } }, { "pH": 8.0, "V": -0.5, "counts": { - "0.1": 6508, - "0.2": 20353, - "0.3": 35568, - "0.4": 48979, - "0.5": 60974 + "0.1": 6530, + "0.2": 20400, + "0.3": 35635, + "0.4": 49067, + "0.5": 61071 } }, { "pH": 8.0, "V": 0.0, "counts": { - "0.1": 7422, - "0.2": 24215, - "0.3": 41154, - "0.4": 55140, - "0.5": 64149 + "0.1": 7461, + "0.2": 24279, + "0.3": 41231, + "0.4": 55236, + "0.5": 64251 } }, { "pH": 8.0, "V": 0.5, "counts": { - "0.1": 7469, - "0.2": 24045, - "0.3": 42511, - "0.4": 54536, - "0.5": 62384 + "0.1": 7511, + "0.2": 24110, + "0.3": 42590, + "0.4": 54631, + "0.5": 62484 } }, { "pH": 8.0, "V": 1.0, "counts": { - "0.1": 5912, - "0.2": 19461, - "0.3": 34562, - "0.4": 46161, - "0.5": 54917 + "0.1": 5941, + "0.2": 19518, + "0.3": 34629, + "0.4": 46238, + "0.5": 54998 } }, { "pH": 8.0, "V": 1.5, "counts": { - "0.1": 5256, - "0.2": 15441, - "0.3": 25529, - "0.4": 34422, - "0.5": 43096 + "0.1": 5272, + "0.2": 15484, + "0.3": 25585, + "0.4": 34484, + "0.5": 43164 } }, { "pH": 8.0, "V": 2.0, "counts": { - "0.1": 5220, - "0.2": 11106, - "0.3": 17829, - "0.4": 24008, - "0.5": 29894 + "0.1": 5229, + "0.2": 11121, + "0.3": 17865, + "0.4": 24048, + "0.5": 29940 } }, { "pH": 9.0, "V": -2.0, "counts": { - "0.1": 4612, - "0.2": 6375, - "0.3": 8269, - "0.4": 10974, - "0.5": 14120 + "0.1": 4613, + "0.2": 6376, + "0.3": 8271, + "0.4": 10980, + "0.5": 14130 } }, { "pH": 9.0, "V": -1.5, "counts": { - "0.1": 5241, - "0.2": 9070, - "0.3": 13705, - "0.4": 20173, - "0.5": 27182 + "0.1": 5247, + "0.2": 9083, + "0.3": 13721, + "0.4": 20195, + "0.5": 27223 } }, { "pH": 9.0, "V": -1.0, "counts": { - "0.1": 4831, - "0.2": 10819, - "0.3": 19805, - "0.4": 31816, - "0.5": 44589 + "0.1": 4842, + "0.2": 10838, + "0.3": 19835, + "0.4": 31874, + "0.5": 44676 } }, { "pH": 9.0, "V": -0.5, "counts": { - "0.1": 7147, - "0.2": 21451, - "0.3": 37950, - "0.4": 52029, - "0.5": 63819 + "0.1": 7168, + "0.2": 21511, + "0.3": 38022, + "0.4": 52121, + "0.5": 63924 } }, { "pH": 9.0, "V": 0.0, "counts": { - "0.1": 7755, - "0.2": 24168, - "0.3": 42100, - "0.4": 56021, - "0.5": 64244 + "0.1": 7790, + "0.2": 24232, + "0.3": 42180, + "0.4": 56119, + "0.5": 64344 } }, { "pH": 9.0, "V": 0.5, "counts": { - "0.1": 7739, - "0.2": 24019, - "0.3": 42235, - "0.4": 54209, - "0.5": 61974 + "0.1": 7775, + "0.2": 24089, + "0.3": 42317, + "0.4": 54303, + "0.5": 62074 } }, { "pH": 9.0, "V": 1.0, "counts": { - "0.1": 6232, - "0.2": 19226, - "0.3": 33558, - "0.4": 44680, - "0.5": 53625 + "0.1": 6256, + "0.2": 19282, + "0.3": 33626, + "0.4": 44758, + "0.5": 53706 } }, { "pH": 9.0, "V": 1.5, "counts": { - "0.1": 5446, - "0.2": 14887, - "0.3": 24415, - "0.4": 33039, - "0.5": 41295 + "0.1": 5462, + "0.2": 14922, + "0.3": 24468, + "0.4": 33101, + "0.5": 41366 } }, { "pH": 9.0, "V": 2.0, "counts": { - "0.1": 5385, - "0.2": 10934, - "0.3": 17085, - "0.4": 23093, - "0.5": 28637 + "0.1": 5393, + "0.2": 10948, + "0.3": 17119, + "0.4": 23134, + "0.5": 28682 } }, { "pH": 10.0, "V": -2.0, "counts": { - "0.1": 4575, - "0.2": 6345, - "0.3": 8570, - "0.4": 11635, - "0.5": 15160 + "0.1": 4576, + "0.2": 6346, + "0.3": 8572, + "0.4": 11641, + "0.5": 15173 } }, { "pH": 10.0, "V": -1.5, "counts": { - "0.1": 5379, - "0.2": 9419, - "0.3": 15092, - "0.4": 22116, - "0.5": 29091 + "0.1": 5385, + "0.2": 9433, + "0.3": 15108, + "0.4": 22143, + "0.5": 29135 } }, { "pH": 10.0, "V": -1.0, "counts": { - "0.1": 5407, - "0.2": 12969, - "0.3": 24438, - "0.4": 36887, - "0.5": 49449 + "0.1": 5418, + "0.2": 12993, + "0.3": 24483, + "0.4": 36956, + "0.5": 49540 } }, { "pH": 10.0, "V": -0.5, "counts": { - "0.1": 7682, - "0.2": 22789, - "0.3": 40533, - "0.4": 54499, - "0.5": 65750 + "0.1": 7710, + "0.2": 22852, + "0.3": 40608, + "0.4": 54589, + "0.5": 65857 } }, { "pH": 10.0, "V": 0.0, "counts": { - "0.1": 7972, - "0.2": 24237, - "0.3": 42595, - "0.4": 55815, - "0.5": 63884 + "0.1": 8004, + "0.2": 24302, + "0.3": 42678, + "0.4": 55914, + "0.5": 63984 } }, { "pH": 10.0, "V": 0.5, "counts": { - "0.1": 7720, - "0.2": 23773, - "0.3": 41540, - "0.4": 53350, - "0.5": 61264 + "0.1": 7754, + "0.2": 23845, + "0.3": 41624, + "0.4": 53445, + "0.5": 61364 } }, { "pH": 10.0, "V": 1.0, "counts": { - "0.1": 6383, - "0.2": 18824, - "0.3": 32649, - "0.4": 43132, - "0.5": 52024 + "0.1": 6407, + "0.2": 18879, + "0.3": 32715, + "0.4": 43211, + "0.5": 52104 } }, { "pH": 10.0, "V": 1.5, "counts": { - "0.1": 5453, - "0.2": 14025, - "0.3": 23111, - "0.4": 31410, - "0.5": 39010 + "0.1": 5467, + "0.2": 14055, + "0.3": 23163, + "0.4": 31470, + "0.5": 39078 } }, { "pH": 10.0, "V": 2.0, "counts": { - "0.1": 5511, - "0.2": 10692, - "0.3": 16419, - "0.4": 22239, - "0.5": 27526 + "0.1": 5519, + "0.2": 10706, + "0.3": 16444, + "0.4": 22280, + "0.5": 27570 } }, { "pH": 11.0, "V": -2.0, "counts": { - "0.1": 4508, - "0.2": 6380, - "0.3": 9120, - "0.4": 12465, - "0.5": 16102 + "0.1": 4509, + "0.2": 6382, + "0.3": 9122, + "0.4": 12474, + "0.5": 16116 } }, { "pH": 11.0, "V": -1.5, "counts": { - "0.1": 5268, - "0.2": 10006, - "0.3": 16470, - "0.4": 23612, - "0.5": 30754 + "0.1": 5274, + "0.2": 10022, + "0.3": 16490, + "0.4": 23641, + "0.5": 30801 } }, { "pH": 11.0, "V": -1.0, "counts": { - "0.1": 5582, - "0.2": 14117, - "0.3": 26859, - "0.4": 39691, - "0.5": 52220 + "0.1": 5594, + "0.2": 14147, + "0.3": 26912, + "0.4": 39766, + "0.5": 52315 } }, { "pH": 11.0, "V": -0.5, "counts": { - "0.1": 7869, - "0.2": 24217, - "0.3": 42027, - "0.4": 55797, - "0.5": 66566 + "0.1": 7896, + "0.2": 24285, + "0.3": 42104, + "0.4": 55889, + "0.5": 66673 } }, { "pH": 11.0, "V": 0.0, "counts": { - "0.1": 7641, - "0.2": 23809, - "0.3": 42512, - "0.4": 55325, - "0.5": 63201 + "0.1": 7671, + "0.2": 23883, + "0.3": 42596, + "0.4": 55424, + "0.5": 63301 } }, { "pH": 11.0, "V": 0.5, "counts": { - "0.1": 7281, - "0.2": 23342, - "0.3": 40593, - "0.4": 51993, - "0.5": 60245 + "0.1": 7308, + "0.2": 23416, + "0.3": 40677, + "0.4": 52087, + "0.5": 60345 } }, { "pH": 11.0, "V": 1.0, "counts": { - "0.1": 6260, - "0.2": 18346, - "0.3": 31744, - "0.4": 41734, - "0.5": 50320 + "0.1": 6285, + "0.2": 18400, + "0.3": 31809, + "0.4": 41814, + "0.5": 50400 } }, { "pH": 11.0, "V": 1.5, "counts": { - "0.1": 5221, - "0.2": 13045, - "0.3": 21845, - "0.4": 29597, - "0.5": 37176 + "0.1": 5233, + "0.2": 13070, + "0.3": 21888, + "0.4": 29652, + "0.5": 37242 } }, { "pH": 11.0, "V": 2.0, "counts": { - "0.1": 5327, - "0.2": 10221, - "0.3": 15672, - "0.4": 21346, - "0.5": 26531 + "0.1": 5335, + "0.2": 10235, + "0.3": 15692, + "0.4": 21382, + "0.5": 26574 } }, { "pH": 12.0, "V": -2.0, "counts": { - "0.1": 4479, - "0.2": 6624, - "0.3": 9659, - "0.4": 13262, - "0.5": 17159 + "0.1": 4480, + "0.2": 6626, + "0.3": 9661, + "0.4": 13271, + "0.5": 17173 } }, { "pH": 12.0, "V": -1.5, "counts": { - "0.1": 5306, - "0.2": 10923, - "0.3": 17407, - "0.4": 24797, - "0.5": 32214 + "0.1": 5311, + "0.2": 10940, + "0.3": 17430, + "0.4": 24828, + "0.5": 32265 } }, { "pH": 12.0, "V": -1.0, "counts": { - "0.1": 5753, - "0.2": 15520, - "0.3": 28977, - "0.4": 42028, - "0.5": 54440 + "0.1": 5765, + "0.2": 15560, + "0.3": 29041, + "0.4": 42104, + "0.5": 54537 } }, { "pH": 12.0, "V": -0.5, "counts": { - "0.1": 7229, - "0.2": 23428, - "0.3": 41080, - "0.4": 55171, - "0.5": 65287 + "0.1": 7251, + "0.2": 23497, + "0.3": 41159, + "0.4": 55266, + "0.5": 65394 } }, { "pH": 12.0, "V": 0.0, "counts": { - "0.1": 7248, - "0.2": 23348, - "0.3": 41763, - "0.4": 54329, - "0.5": 62388 + "0.1": 7272, + "0.2": 23423, + "0.3": 41846, + "0.4": 54426, + "0.5": 62488 } }, { "pH": 12.0, "V": 0.5, "counts": { - "0.1": 6853, - "0.2": 22856, - "0.3": 39215, - "0.4": 50321, - "0.5": 58870 + "0.1": 6875, + "0.2": 22928, + "0.3": 39297, + "0.4": 50413, + "0.5": 58970 } }, { "pH": 12.0, "V": 1.0, "counts": { - "0.1": 5936, - "0.2": 17245, - "0.3": 29954, - "0.4": 39968, - "0.5": 48205 + "0.1": 5954, + "0.2": 17295, + "0.3": 30018, + "0.4": 40047, + "0.5": 48285 } }, { "pH": 12.0, "V": 1.5, "counts": { - "0.1": 5106, - "0.2": 12146, - "0.3": 20624, - "0.4": 27920, - "0.5": 35333 + "0.1": 5116, + "0.2": 12167, + "0.3": 20663, + "0.4": 27974, + "0.5": 35400 } }, { "pH": 12.0, "V": 2.0, "counts": { - "0.1": 5156, - "0.2": 9489, - "0.3": 14728, - "0.4": 20340, - "0.5": 25380 + "0.1": 5163, + "0.2": 9503, + "0.3": 14743, + "0.4": 20376, + "0.5": 25422 } }, { "pH": 13.0, "V": -2.0, "counts": { - "0.1": 4501, - "0.2": 7042, - "0.3": 10465, - "0.4": 14305, - "0.5": 18358 + "0.1": 4502, + "0.2": 7044, + "0.3": 10470, + "0.4": 14319, + "0.5": 18372 } }, { "pH": 13.0, "V": -1.5, "counts": { - "0.1": 5628, - "0.2": 11474, - "0.3": 18087, - "0.4": 25546, - "0.5": 33826 + "0.1": 5634, + "0.2": 11491, + "0.3": 18111, + "0.4": 25580, + "0.5": 33878 } }, { "pH": 13.0, "V": -1.0, "counts": { - "0.1": 5614, - "0.2": 16402, - "0.3": 30728, - "0.4": 43535, - "0.5": 55510 + "0.1": 5627, + "0.2": 16442, + "0.3": 30796, + "0.4": 43612, + "0.5": 55602 } }, { "pH": 13.0, "V": -0.5, "counts": { - "0.1": 6499, - "0.2": 21639, - "0.3": 39549, - "0.4": 54048, - "0.5": 63601 + "0.1": 6521, + "0.2": 21706, + "0.3": 39633, + "0.4": 54140, + "0.5": 63705 } }, { "pH": 13.0, "V": 0.0, "counts": { - "0.1": 6937, - "0.2": 22251, - "0.3": 40514, - "0.4": 52931, - "0.5": 61330 + "0.1": 6961, + "0.2": 22323, + "0.3": 40597, + "0.4": 53026, + "0.5": 61429 } }, { "pH": 13.0, "V": 0.5, "counts": { - "0.1": 6550, - "0.2": 21634, - "0.3": 37526, - "0.4": 48177, - "0.5": 56671 + "0.1": 6572, + "0.2": 21697, + "0.3": 37606, + "0.4": 48262, + "0.5": 56762 } }, { "pH": 13.0, "V": 1.0, "counts": { - "0.1": 5434, - "0.2": 15533, - "0.3": 27707, - "0.4": 37362, - "0.5": 45339 + "0.1": 5452, + "0.2": 15575, + "0.3": 27770, + "0.4": 37440, + "0.5": 45419 } }, { "pH": 13.0, "V": 1.5, "counts": { - "0.1": 4884, - "0.2": 11052, - "0.3": 19103, - "0.4": 25866, - "0.5": 33147 + "0.1": 4892, + "0.2": 11069, + "0.3": 19136, + "0.4": 25912, + "0.5": 33206 } }, { "pH": 13.0, "V": 2.0, "counts": { - "0.1": 4998, - "0.2": 8678, - "0.3": 13741, - "0.4": 19263, - "0.5": 24133 + "0.1": 5005, + "0.2": 8687, + "0.3": 13755, + "0.4": 19293, + "0.5": 24175 } }, { "pH": 14.0, "V": -2.0, "counts": { - "0.1": 4537, - "0.2": 7770, - "0.3": 11448, - "0.4": 15576, - "0.5": 20015 + "0.1": 4538, + "0.2": 7773, + "0.3": 11454, + "0.4": 15590, + "0.5": 20031 } }, { "pH": 14.0, "V": -1.5, "counts": { - "0.1": 5416, - "0.2": 11385, - "0.3": 18043, - "0.4": 25791, - "0.5": 35107 + "0.1": 5423, + "0.2": 11400, + "0.3": 18068, + "0.4": 25825, + "0.5": 35166 } }, { "pH": 14.0, "V": -1.0, "counts": { - "0.1": 5528, - "0.2": 16391, - "0.3": 30722, - "0.4": 43706, - "0.5": 56402 + "0.1": 5541, + "0.2": 16436, + "0.3": 30791, + "0.4": 43784, + "0.5": 56496 } }, { "pH": 14.0, "V": -0.5, "counts": { - "0.1": 5999, - "0.2": 19884, - "0.3": 37429, - "0.4": 52302, - "0.5": 62172 + "0.1": 6021, + "0.2": 19946, + "0.3": 37515, + "0.4": 52394, + "0.5": 62275 } }, { "pH": 14.0, "V": 0.0, "counts": { - "0.1": 6412, - "0.2": 20627, - "0.3": 38600, - "0.4": 50844, - "0.5": 59654 + "0.1": 6438, + "0.2": 20695, + "0.3": 38683, + "0.4": 50936, + "0.5": 59753 } }, { "pH": 14.0, "V": 0.5, "counts": { - "0.1": 5971, - "0.2": 20014, - "0.3": 35275, - "0.4": 45868, - "0.5": 54328 + "0.1": 5994, + "0.2": 20074, + "0.3": 35354, + "0.4": 45953, + "0.5": 54416 } }, { "pH": 14.0, "V": 1.0, "counts": { - "0.1": 4888, - "0.2": 13583, - "0.3": 25291, - "0.4": 34560, - "0.5": 42304 + "0.1": 4903, + "0.2": 13615, + "0.3": 25348, + "0.4": 34631, + "0.5": 42384 } }, { "pH": 14.0, "V": 1.5, "counts": { - "0.1": 4681, - "0.2": 10060, - "0.3": 17537, - "0.4": 24170, - "0.5": 30658 + "0.1": 4688, + "0.2": 10074, + "0.3": 17564, + "0.4": 24215, + "0.5": 30716 } }, { "pH": 14.0, "V": 2.0, "counts": { - "0.1": 4801, - "0.2": 8005, - "0.3": 12732, - "0.4": 17965, - "0.5": 22797 + "0.1": 4807, + "0.2": 8014, + "0.3": 12746, + "0.4": 17986, + "0.5": 22835 } } ] -} \ No newline at end of file +} From d27f1cd1b72d4663d0260a3aba28dd8e6f6a8be2 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 28 May 2026 11:52:42 -0700 Subject: [PATCH 21/33] small layout changes --- .../components/reverse_pourbaix.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index c0e82bd7..083d958b 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -31,8 +31,8 @@ __author__ = "Leo Karlsson" # Grid and display constants -HEIGHT = 550 -WIDTH = 700 +#HEIGHT = 550 +#WIDTH = 700 MIN_PH = 0 MAX_PH = 14 MIN_V = -2 @@ -83,7 +83,7 @@ class ReversePourbaixDiagramComponent(MPComponent): "range": [MIN_PH, MAX_PH], }, yaxis={ - "title": "Potential (V vs. SHE)", + "title": "Potential (V vs. SHE TEST)", "anchor": "x", "mirror": "ticks", "range": [MIN_V, MAX_V], @@ -101,11 +101,12 @@ class ReversePourbaixDiagramComponent(MPComponent): }, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", - height=HEIGHT, - width=WIDTH, + autosize=True, hovermode="closest", showlegend=False, margin=dict(l=80, b=70, t=10, r=20), + # height=HEIGHT, + # width=WIDTH, ) empty_plot_style = frozendict( @@ -268,15 +269,14 @@ def _sub_layouts(self) -> dict[str, Component]: [ dcc.Graph( id=self.id("heatmap"), - figure=go.Figure( - layout={**ReversePourbaixDiagramComponent.empty_plot_style} - ), + figure=go.Figure(layout={**ReversePourbaixDiagramComponent.empty_plot_style}), responsive=True, - config={"displayModeBar": False, "displaylogo": False}, + config={"displayModeBar": False, "displaylogo": False, "responsive": True}, + style={"height": "100%", "width": "100%"}, ), ], - # style={"minHeight": "500px"}, id=self.id("graph-panel"), + style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) # Holds the list of mp_ids stable at the most recently clicked cell. From b0126ee95f8eaaadfc13baa10b9deb1a1f503774 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 28 May 2026 15:57:48 -0700 Subject: [PATCH 22/33] updated formatting --- crystal_toolkit/components/reverse_pourbaix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 083d958b..6ad13a14 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -83,7 +83,7 @@ class ReversePourbaixDiagramComponent(MPComponent): "range": [MIN_PH, MAX_PH], }, yaxis={ - "title": "Potential (V vs. SHE TEST)", + "title": "Potential (V vs. SHE)", "anchor": "x", "mirror": "ticks", "range": [MIN_V, MAX_V], @@ -272,11 +272,11 @@ def _sub_layouts(self) -> dict[str, Component]: figure=go.Figure(layout={**ReversePourbaixDiagramComponent.empty_plot_style}), responsive=True, config={"displayModeBar": False, "displaylogo": False, "responsive": True}, - style={"height": "100%", "width": "100%"}, + style={"flex": "1 1 auto", "height": "100%", "width": "100%"} ), ], id=self.id("graph-panel"), - style={"height": "100%", "display": "flex", "flexDirection": "column"}, + style={"display": "flex", "flexDirection": "column", "flex": "1 1 auto"}, ) # Holds the list of mp_ids stable at the most recently clicked cell. From 030608217a286936fbeb6d2fbf50d8c7f8775f0c Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 3 Jun 2026 15:25:10 -0700 Subject: [PATCH 23/33] updated formatting, fixed plotly issue --- .../components/reverse_pourbaix.py | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 6ad13a14..b72be260 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -31,8 +31,14 @@ __author__ = "Leo Karlsson" # Grid and display constants -#HEIGHT = 550 -#WIDTH = 700 +# Height of the heatmap graph. A *definite* height (rather than height:100%) +# is important: the graph is embedded in the web app inside a dcc.Loading +# wrapper, which has no height of its own and so breaks any height:100% chain. +# With autosize and no definite height, each redraw (e.g. moving the stability +# cutoff) resolved to a different height and the white tile flickered between +# full height and ~450px. A fixed height makes every redraw deterministic. +HEATMAP_HEIGHT = "70vh" +# WIDTH = 700 MIN_PH = 0 MAX_PH = 14 MIN_V = -2 @@ -105,8 +111,9 @@ class ReversePourbaixDiagramComponent(MPComponent): hovermode="closest", showlegend=False, margin=dict(l=80, b=70, t=10, r=20), - # height=HEIGHT, - # width=WIDTH, + # height intentionally not set here: the height is fixed via the + # dcc.Graph CSS style (HEATMAP_HEIGHT) so Plotly's responsive autosize + # has a definite container height to fill on every redraw. ) empty_plot_style = frozendict( @@ -269,14 +276,24 @@ def _sub_layouts(self) -> dict[str, Component]: [ dcc.Graph( id=self.id("heatmap"), - figure=go.Figure(layout={**ReversePourbaixDiagramComponent.empty_plot_style}), + figure=go.Figure( + layout={**ReversePourbaixDiagramComponent.empty_plot_style} + ), responsive=True, - config={"displayModeBar": False, "displaylogo": False, "responsive": True}, - style={"flex": "1 1 auto", "height": "100%", "width": "100%"} + config={ + "displayModeBar": False, + "displaylogo": False, + "responsive": True, + }, + # Definite height (see HEATMAP_HEIGHT note above). This used + # to be height:100% + flex fill, which depends on a definite- + # height ancestor that the web app's dcc.Loading wrapper does + # not provide, so the autosize height was non-deterministic + # and the tile flickered on each cutoff redraw. + style={"height": HEATMAP_HEIGHT, "width": "100%"}, ), ], id=self.id("graph-panel"), - style={"display": "flex", "flexDirection": "column", "flex": "1 1 auto"}, ) # Holds the list of mp_ids stable at the most recently clicked cell. @@ -395,4 +412,4 @@ def update_figure( show_water_lines=bool(show_water_lines), ) - return figure + return figure \ No newline at end of file From 5ea1f7881a37d1e8a60d821a9923dd8deadce95b Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 8 Jun 2026 14:05:28 -0700 Subject: [PATCH 24/33] small updates to comment and removal of dummy filters, also updated json data is included --- .../examples/reverse_pourbaix_heatmap.json | 1164 ++++++++--------- .../components/reverse_pourbaix.py | 57 - 2 files changed, 582 insertions(+), 639 deletions(-) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json index 3c0a8ffb..1b9b7011 100644 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json @@ -54,7 +54,7 @@ "0.2": 5138, "0.3": 6649, "0.4": 8839, - "0.5": 11708 + "0.5": 11709 } }, { @@ -64,8 +64,8 @@ "0.1": 3040, "0.2": 4670, "0.3": 7284, - "0.4": 11428, - "0.5": 17109 + "0.4": 11430, + "0.5": 17112 } }, { @@ -75,63 +75,63 @@ "0.1": 2326, "0.2": 4953, "0.3": 9906, - "0.4": 16986, - "0.5": 26486 + "0.4": 16989, + "0.5": 26490 } }, { "pH": 0.0, "V": 0.0, "counts": { - "0.1": 2070, - "0.2": 7227, - "0.3": 18532, - "0.4": 31182, - "0.5": 43223 + "0.1": 2071, + "0.2": 7228, + "0.3": 18534, + "0.4": 31187, + "0.5": 43231 } }, { "pH": 0.0, "V": 0.5, "counts": { - "0.1": 1770, - "0.2": 8917, - "0.3": 22613, - "0.4": 35745, - "0.5": 45824 + "0.1": 1771, + "0.2": 8919, + "0.3": 22616, + "0.4": 35752, + "0.5": 45833 } }, { "pH": 0.0, "V": 1.0, "counts": { - "0.1": 2094, - "0.2": 9665, - "0.3": 24016, - "0.4": 37245, - "0.5": 47887 + "0.1": 2095, + "0.2": 9667, + "0.3": 24020, + "0.4": 37254, + "0.5": 47898 } }, { "pH": 0.0, "V": 1.5, "counts": { - "0.1": 2070, - "0.2": 9492, - "0.3": 22465, - "0.4": 34818, - "0.5": 46223 + "0.1": 2071, + "0.2": 9495, + "0.3": 22469, + "0.4": 34824, + "0.5": 46232 } }, { "pH": 0.0, "V": 2.0, "counts": { - "0.1": 2382, - "0.2": 8672, - "0.3": 19276, - "0.4": 28691, - "0.5": 37967 + "0.1": 2383, + "0.2": 8674, + "0.3": 19278, + "0.4": 28695, + "0.5": 37971 } }, { @@ -152,8 +152,8 @@ "0.1": 3857, "0.2": 5279, "0.3": 6973, - "0.4": 9419, - "0.5": 12638 + "0.4": 9420, + "0.5": 12639 } }, { @@ -163,8 +163,8 @@ "0.1": 3012, "0.2": 4935, "0.3": 8051, - "0.4": 12753, - "0.5": 19065 + "0.4": 12755, + "0.5": 19068 } }, { @@ -173,64 +173,64 @@ "counts": { "0.1": 2617, "0.2": 6018, - "0.3": 12110, - "0.4": 21213, - "0.5": 32793 + "0.3": 12111, + "0.4": 21216, + "0.5": 32797 } }, { "pH": 1.0, "V": 0.0, "counts": { - "0.1": 2458, - "0.2": 9179, - "0.3": 22267, - "0.4": 36252, - "0.5": 47966 + "0.1": 2459, + "0.2": 9181, + "0.3": 22269, + "0.4": 36258, + "0.5": 47974 } }, { "pH": 1.0, "V": 0.5, "counts": { - "0.1": 2490, - "0.2": 11383, - "0.3": 26450, - "0.4": 38609, - "0.5": 48801 + "0.1": 2491, + "0.2": 11385, + "0.3": 26453, + "0.4": 38617, + "0.5": 48812 } }, { "pH": 1.0, "V": 1.0, "counts": { - "0.1": 2814, - "0.2": 11998, - "0.3": 27781, - "0.4": 40641, - "0.5": 52143 + "0.1": 2815, + "0.2": 12000, + "0.3": 27788, + "0.4": 40651, + "0.5": 52154 } }, { "pH": 1.0, "V": 1.5, "counts": { - "0.1": 2729, - "0.2": 11765, - "0.3": 24939, - "0.4": 37641, - "0.5": 47745 + "0.1": 2730, + "0.2": 11768, + "0.3": 24943, + "0.4": 37647, + "0.5": 47754 } }, { "pH": 1.0, "V": 2.0, "counts": { - "0.1": 2950, - "0.2": 9973, - "0.3": 20418, - "0.4": 29592, - "0.5": 38457 + "0.1": 2951, + "0.2": 9975, + "0.3": 20420, + "0.4": 29596, + "0.5": 38461 } }, { @@ -251,8 +251,8 @@ "0.1": 3902, "0.2": 5503, "0.3": 7457, - "0.4": 10210, - "0.5": 13817 + "0.4": 10211, + "0.5": 13818 } }, { @@ -261,9 +261,9 @@ "counts": { "0.1": 3044, "0.2": 5353, - "0.3": 9139, - "0.4": 14581, - "0.5": 21471 + "0.3": 9140, + "0.4": 14583, + "0.5": 21474 } }, { @@ -271,65 +271,65 @@ "V": -0.5, "counts": { "0.1": 3068, - "0.2": 7324, - "0.3": 15543, - "0.4": 27227, - "0.5": 38241 + "0.2": 7325, + "0.3": 15545, + "0.4": 27231, + "0.5": 38250 } }, { "pH": 2.0, "V": 0.0, "counts": { - "0.1": 2793, - "0.2": 11489, - "0.3": 27036, - "0.4": 41037, - "0.5": 51239 + "0.1": 2794, + "0.2": 11491, + "0.3": 27040, + "0.4": 41045, + "0.5": 51247 } }, { "pH": 2.0, "V": 0.5, "counts": { - "0.1": 3405, - "0.2": 14409, - "0.3": 30003, - "0.4": 41935, - "0.5": 51779 + "0.1": 3406, + "0.2": 14411, + "0.3": 30009, + "0.4": 41943, + "0.5": 51791 } }, { "pH": 2.0, "V": 1.0, "counts": { - "0.1": 3522, - "0.2": 14764, - "0.3": 31037, - "0.4": 43820, - "0.5": 54652 + "0.1": 3523, + "0.2": 14766, + "0.3": 31045, + "0.4": 43830, + "0.5": 54663 } }, { "pH": 2.0, "V": 1.5, "counts": { - "0.1": 3446, - "0.2": 13321, - "0.3": 26166, - "0.4": 38525, - "0.5": 48237 + "0.1": 3447, + "0.2": 13323, + "0.3": 26170, + "0.4": 38531, + "0.5": 48246 } }, { "pH": 2.0, "V": 2.0, "counts": { - "0.1": 3196, - "0.2": 10399, - "0.3": 20945, - "0.4": 29685, - "0.5": 38227 + "0.1": 3197, + "0.2": 10401, + "0.3": 20948, + "0.4": 29689, + "0.5": 38231 } }, { @@ -349,9 +349,9 @@ "counts": { "0.1": 4000, "0.2": 5807, - "0.3": 8040, - "0.4": 11399, - "0.5": 15059 + "0.3": 8041, + "0.4": 11400, + "0.5": 15061 } }, { @@ -360,75 +360,75 @@ "counts": { "0.1": 3171, "0.2": 5950, - "0.3": 10480, - "0.4": 16502, - "0.5": 23614 + "0.3": 10481, + "0.4": 16504, + "0.5": 23618 } }, { "pH": 3.0, "V": -0.5, "counts": { - "0.1": 3757, - "0.2": 10028, - "0.3": 21323, - "0.4": 32598, - "0.5": 43305 + "0.1": 3758, + "0.2": 10029, + "0.3": 21326, + "0.4": 32603, + "0.5": 43314 } }, { "pH": 3.0, "V": 0.0, "counts": { - "0.1": 3413, - "0.2": 15033, - "0.3": 32234, - "0.4": 44650, - "0.5": 54388 + "0.1": 3414, + "0.2": 15035, + "0.3": 32239, + "0.4": 44658, + "0.5": 54396 } }, { "pH": 3.0, "V": 0.5, "counts": { - "0.1": 4430, - "0.2": 17573, - "0.3": 32999, - "0.4": 45247, - "0.5": 55586 + "0.1": 4431, + "0.2": 17575, + "0.3": 33006, + "0.4": 45258, + "0.5": 55598 } }, { "pH": 3.0, "V": 1.0, "counts": { - "0.1": 4287, - "0.2": 17480, - "0.3": 33589, - "0.4": 47038, - "0.5": 56391 + "0.1": 4288, + "0.2": 17484, + "0.3": 33597, + "0.4": 47048, + "0.5": 56402 } }, { "pH": 3.0, "V": 1.5, "counts": { - "0.1": 4102, - "0.2": 14468, - "0.3": 26877, - "0.4": 38718, - "0.5": 48165 + "0.1": 4103, + "0.2": 14470, + "0.3": 26881, + "0.4": 38723, + "0.5": 48174 } }, { "pH": 3.0, "V": 2.0, "counts": { - "0.1": 3358, - "0.2": 10560, - "0.3": 20505, - "0.4": 28922, - "0.5": 37011 + "0.1": 3359, + "0.2": 10562, + "0.3": 20508, + "0.4": 28926, + "0.5": 37015 } }, { @@ -448,9 +448,9 @@ "counts": { "0.1": 4108, "0.2": 6137, - "0.3": 8885, - "0.4": 12396, - "0.5": 16508 + "0.3": 8886, + "0.4": 12398, + "0.5": 16510 } }, { @@ -458,76 +458,76 @@ "V": -1.0, "counts": { "0.1": 3336, - "0.2": 6793, - "0.3": 11905, - "0.4": 18241, - "0.5": 25588 + "0.2": 6794, + "0.3": 11907, + "0.4": 18243, + "0.5": 25593 } }, { "pH": 4.0, "V": -0.5, "counts": { - "0.1": 4379, - "0.2": 12916, - "0.3": 25073, - "0.4": 36267, - "0.5": 47266 + "0.1": 4380, + "0.2": 12918, + "0.3": 25076, + "0.4": 36275, + "0.5": 47278 } }, { "pH": 4.0, "V": 0.0, "counts": { - "0.1": 4438, - "0.2": 18891, - "0.3": 35714, - "0.4": 47495, - "0.5": 57225 + "0.1": 4439, + "0.2": 18894, + "0.3": 35721, + "0.4": 47503, + "0.5": 57235 } }, { "pH": 4.0, "V": 0.5, "counts": { - "0.1": 5218, - "0.2": 19351, - "0.3": 35454, - "0.4": 47762, - "0.5": 58526 + "0.1": 5219, + "0.2": 19356, + "0.3": 35462, + "0.4": 47774, + "0.5": 58538 } }, { "pH": 4.0, "V": 1.0, "counts": { - "0.1": 4944, - "0.2": 18659, - "0.3": 35045, - "0.4": 47885, - "0.5": 57050 + "0.1": 4945, + "0.2": 18664, + "0.3": 35053, + "0.4": 47895, + "0.5": 57061 } }, { "pH": 4.0, "V": 1.5, "counts": { - "0.1": 4495, - "0.2": 15322, - "0.3": 27279, - "0.4": 38495, - "0.5": 47331 + "0.1": 4496, + "0.2": 15325, + "0.3": 27283, + "0.4": 38499, + "0.5": 47339 } }, { "pH": 4.0, "V": 2.0, "counts": { - "0.1": 3814, - "0.2": 10801, - "0.3": 20358, - "0.4": 27877, - "0.5": 35687 + "0.1": 3815, + "0.2": 10804, + "0.3": 20361, + "0.4": 27881, + "0.5": 35691 } }, { @@ -546,10 +546,10 @@ "V": -1.5, "counts": { "0.1": 4270, - "0.2": 6621, - "0.3": 9696, - "0.4": 13626, - "0.5": 17977 + "0.2": 6622, + "0.3": 9698, + "0.4": 13628, + "0.5": 17979 } }, { @@ -557,76 +557,76 @@ "V": -1.0, "counts": { "0.1": 3697, - "0.2": 7777, - "0.3": 13219, - "0.4": 19789, - "0.5": 27819 + "0.2": 7778, + "0.3": 13221, + "0.4": 19792, + "0.5": 27826 } }, { "pH": 5.0, "V": -0.5, "counts": { - "0.1": 4979, - "0.2": 15348, - "0.3": 28245, - "0.4": 40084, - "0.5": 50601 + "0.1": 4980, + "0.2": 15349, + "0.3": 28249, + "0.4": 40093, + "0.5": 50613 } }, { "pH": 5.0, "V": 0.0, "counts": { - "0.1": 5422, - "0.2": 21159, - "0.3": 37274, - "0.4": 48880, - "0.5": 58995 + "0.1": 5423, + "0.2": 21163, + "0.3": 37281, + "0.4": 48889, + "0.5": 59007 } }, { "pH": 5.0, "V": 0.5, "counts": { - "0.1": 5944, - "0.2": 20659, - "0.3": 37558, - "0.4": 51055, - "0.5": 60410 + "0.1": 5945, + "0.2": 20665, + "0.3": 37567, + "0.4": 51067, + "0.5": 60422 } }, { "pH": 5.0, "V": 1.0, "counts": { - "0.1": 5455, - "0.2": 19298, - "0.3": 35325, - "0.4": 48118, - "0.5": 56731 + "0.1": 5456, + "0.2": 19302, + "0.3": 35333, + "0.4": 48127, + "0.5": 56742 } }, { "pH": 5.0, "V": 1.5, "counts": { - "0.1": 4967, - "0.2": 16089, - "0.3": 27600, - "0.4": 38097, - "0.5": 46690 + "0.1": 4968, + "0.2": 16092, + "0.3": 27604, + "0.4": 38101, + "0.5": 46697 } }, { "pH": 5.0, "V": 2.0, "counts": { - "0.1": 4256, - "0.2": 10986, - "0.3": 19916, - "0.4": 26924, - "0.5": 34267 + "0.1": 4257, + "0.2": 10989, + "0.3": 19919, + "0.4": 26928, + "0.5": 34271 } }, { @@ -645,10 +645,10 @@ "V": -1.5, "counts": { "0.1": 4371, - "0.2": 7208, - "0.3": 10588, - "0.4": 14773, - "0.5": 19632 + "0.2": 7209, + "0.3": 10590, + "0.4": 14775, + "0.5": 19634 } }, { @@ -656,76 +656,76 @@ "V": -1.0, "counts": { "0.1": 3931, - "0.2": 8539, - "0.3": 14301, - "0.4": 21228, - "0.5": 30354 + "0.2": 8540, + "0.3": 14303, + "0.4": 21231, + "0.5": 30362 } }, { "pH": 6.0, "V": -0.5, "counts": { - "0.1": 5553, - "0.2": 17447, - "0.3": 31222, - "0.4": 43284, - "0.5": 54003 + "0.1": 5554, + "0.2": 17450, + "0.3": 31227, + "0.4": 43293, + "0.5": 54016 } }, { "pH": 6.0, "V": 0.0, "counts": { - "0.1": 6508, - "0.2": 23319, - "0.3": 38946, - "0.4": 50580, - "0.5": 61877 + "0.1": 6509, + "0.2": 23325, + "0.3": 38953, + "0.4": 50591, + "0.5": 61890 } }, { "pH": 6.0, "V": 0.5, "counts": { - "0.1": 6610, - "0.2": 22337, - "0.3": 39754, - "0.4": 53032, - "0.5": 61879 + "0.1": 6611, + "0.2": 22344, + "0.3": 39764, + "0.4": 53044, + "0.5": 61892 } }, { "pH": 6.0, "V": 1.0, "counts": { - "0.1": 5709, - "0.2": 19679, - "0.3": 35405, - "0.4": 47916, - "0.5": 56385 + "0.1": 5710, + "0.2": 19683, + "0.3": 35413, + "0.4": 47925, + "0.5": 56396 } }, { "pH": 6.0, "V": 1.5, "counts": { - "0.1": 5409, - "0.2": 16905, - "0.3": 27893, - "0.4": 37360, - "0.5": 45952 + "0.1": 5410, + "0.2": 16908, + "0.3": 27897, + "0.4": 37364, + "0.5": 45960 } }, { "pH": 6.0, "V": 2.0, "counts": { - "0.1": 4650, - "0.2": 11128, - "0.3": 19227, - "0.4": 25921, - "0.5": 32778 + "0.1": 4651, + "0.2": 11131, + "0.3": 19230, + "0.4": 25925, + "0.5": 32782 } }, { @@ -736,18 +736,18 @@ "0.2": 6110, "0.3": 7758, "0.4": 9785, - "0.5": 12267 + "0.5": 12268 } }, { "pH": 7.0, "V": -1.5, "counts": { - "0.1": 4675, - "0.2": 7787, - "0.3": 11559, - "0.4": 16150, - "0.5": 22053 + "0.1": 4676, + "0.2": 7789, + "0.3": 11561, + "0.4": 16152, + "0.5": 22056 } }, { @@ -755,76 +755,76 @@ "V": -1.0, "counts": { "0.1": 4140, - "0.2": 9037, - "0.3": 15382, - "0.4": 23249, - "0.5": 34160 + "0.2": 9038, + "0.3": 15384, + "0.4": 23253, + "0.5": 34168 } }, { "pH": 7.0, "V": -0.5, "counts": { - "0.1": 6164, - "0.2": 19239, - "0.3": 33709, - "0.4": 45948, - "0.5": 57734 + "0.1": 6165, + "0.2": 19242, + "0.3": 33716, + "0.4": 45957, + "0.5": 57747 } }, { "pH": 7.0, "V": 0.0, "counts": { - "0.1": 7341, - "0.2": 24262, - "0.3": 40594, - "0.4": 53246, - "0.5": 63707 + "0.1": 7342, + "0.2": 24268, + "0.3": 40601, + "0.4": 53258, + "0.5": 63720 } }, { "pH": 7.0, "V": 0.5, "counts": { - "0.1": 7327, - "0.2": 23914, - "0.3": 42281, - "0.4": 54227, - "0.5": 62497 + "0.1": 7328, + "0.2": 23922, + "0.3": 42292, + "0.4": 54238, + "0.5": 62510 } }, { "pH": 7.0, "V": 1.0, "counts": { - "0.1": 5937, - "0.2": 19791, - "0.3": 35502, - "0.4": 47349, - "0.5": 55979 + "0.1": 5938, + "0.2": 19795, + "0.3": 35510, + "0.4": 47358, + "0.5": 55990 } }, { "pH": 7.0, "V": 1.5, "counts": { - "0.1": 5273, - "0.2": 16246, - "0.3": 26893, - "0.4": 35899, - "0.5": 44720 + "0.1": 5274, + "0.2": 16249, + "0.3": 26897, + "0.4": 35903, + "0.5": 44727 } }, { "pH": 7.0, "V": 2.0, "counts": { - "0.1": 5017, - "0.2": 11220, - "0.3": 18630, - "0.4": 25004, - "0.5": 31343 + "0.1": 5018, + "0.2": 11223, + "0.3": 18633, + "0.4": 25008, + "0.5": 31347 } }, { @@ -835,18 +835,18 @@ "0.2": 6247, "0.3": 7997, "0.4": 10194, - "0.5": 13205 + "0.5": 13206 } }, { "pH": 8.0, "V": -1.5, "counts": { - "0.1": 4936, - "0.2": 8420, - "0.3": 12682, - "0.4": 17916, - "0.5": 24867 + "0.1": 4937, + "0.2": 8422, + "0.3": 12684, + "0.4": 17918, + "0.5": 24872 } }, { @@ -854,76 +854,76 @@ "V": -1.0, "counts": { "0.1": 4478, - "0.2": 9786, - "0.3": 16889, - "0.4": 26920, - "0.5": 39457 + "0.2": 9787, + "0.3": 16891, + "0.4": 26928, + "0.5": 39468 } }, { "pH": 8.0, "V": -0.5, "counts": { - "0.1": 6530, - "0.2": 20400, - "0.3": 35635, - "0.4": 49067, - "0.5": 61071 + "0.1": 6531, + "0.2": 20403, + "0.3": 35643, + "0.4": 49078, + "0.5": 61086 } }, { "pH": 8.0, "V": 0.0, "counts": { - "0.1": 7461, - "0.2": 24279, - "0.3": 41231, - "0.4": 55236, - "0.5": 64251 + "0.1": 7462, + "0.2": 24285, + "0.3": 41239, + "0.4": 55249, + "0.5": 64264 } }, { "pH": 8.0, "V": 0.5, "counts": { - "0.1": 7511, - "0.2": 24110, - "0.3": 42590, - "0.4": 54631, - "0.5": 62484 + "0.1": 7512, + "0.2": 24118, + "0.3": 42601, + "0.4": 54643, + "0.5": 62497 } }, { "pH": 8.0, "V": 1.0, "counts": { - "0.1": 5941, - "0.2": 19518, - "0.3": 34629, - "0.4": 46238, - "0.5": 54998 + "0.1": 5942, + "0.2": 19522, + "0.3": 34636, + "0.4": 46248, + "0.5": 55009 } }, { "pH": 8.0, "V": 1.5, "counts": { - "0.1": 5272, - "0.2": 15484, - "0.3": 25585, - "0.4": 34484, - "0.5": 43164 + "0.1": 5273, + "0.2": 15487, + "0.3": 25589, + "0.4": 34489, + "0.5": 43169 } }, { "pH": 8.0, "V": 2.0, "counts": { - "0.1": 5229, - "0.2": 11121, - "0.3": 17865, - "0.4": 24048, - "0.5": 29940 + "0.1": 5230, + "0.2": 11124, + "0.3": 17868, + "0.4": 24052, + "0.5": 29944 } }, { @@ -933,96 +933,96 @@ "0.1": 4613, "0.2": 6376, "0.3": 8271, - "0.4": 10980, - "0.5": 14130 + "0.4": 10981, + "0.5": 14131 } }, { "pH": 9.0, "V": -1.5, "counts": { - "0.1": 5247, - "0.2": 9083, - "0.3": 13721, - "0.4": 20195, - "0.5": 27223 + "0.1": 5249, + "0.2": 9085, + "0.3": 13723, + "0.4": 20197, + "0.5": 27228 } }, { "pH": 9.0, "V": -1.0, "counts": { - "0.1": 4842, - "0.2": 10838, - "0.3": 19835, - "0.4": 31874, - "0.5": 44676 + "0.1": 4843, + "0.2": 10839, + "0.3": 19839, + "0.4": 31882, + "0.5": 44689 } }, { "pH": 9.0, "V": -0.5, "counts": { - "0.1": 7168, - "0.2": 21511, - "0.3": 38022, - "0.4": 52121, - "0.5": 63924 + "0.1": 7170, + "0.2": 21517, + "0.3": 38030, + "0.4": 52132, + "0.5": 63941 } }, { "pH": 9.0, "V": 0.0, "counts": { - "0.1": 7790, - "0.2": 24232, - "0.3": 42180, - "0.4": 56119, - "0.5": 64344 + "0.1": 7792, + "0.2": 24238, + "0.3": 42191, + "0.4": 56132, + "0.5": 64357 } }, { "pH": 9.0, "V": 0.5, "counts": { - "0.1": 7775, - "0.2": 24089, - "0.3": 42317, - "0.4": 54303, - "0.5": 62074 + "0.1": 7777, + "0.2": 24097, + "0.3": 42329, + "0.4": 54315, + "0.5": 62087 } }, { "pH": 9.0, "V": 1.0, "counts": { - "0.1": 6256, - "0.2": 19282, - "0.3": 33626, - "0.4": 44758, - "0.5": 53706 + "0.1": 6258, + "0.2": 19286, + "0.3": 33633, + "0.4": 44767, + "0.5": 53717 } }, { "pH": 9.0, "V": 1.5, "counts": { - "0.1": 5462, - "0.2": 14922, - "0.3": 24468, - "0.4": 33101, - "0.5": 41366 + "0.1": 5464, + "0.2": 14925, + "0.3": 24472, + "0.4": 33106, + "0.5": 41371 } }, { "pH": 9.0, "V": 2.0, "counts": { - "0.1": 5393, - "0.2": 10948, - "0.3": 17119, - "0.4": 23134, - "0.5": 28682 + "0.1": 5395, + "0.2": 10951, + "0.3": 17122, + "0.4": 23138, + "0.5": 28686 } }, { @@ -1032,96 +1032,96 @@ "0.1": 4576, "0.2": 6346, "0.3": 8572, - "0.4": 11641, - "0.5": 15173 + "0.4": 11642, + "0.5": 15174 } }, { "pH": 10.0, "V": -1.5, "counts": { - "0.1": 5385, - "0.2": 9433, - "0.3": 15108, - "0.4": 22143, - "0.5": 29135 + "0.1": 5387, + "0.2": 9435, + "0.3": 15110, + "0.4": 22147, + "0.5": 29141 } }, { "pH": 10.0, "V": -1.0, "counts": { - "0.1": 5418, - "0.2": 12993, - "0.3": 24483, - "0.4": 36956, - "0.5": 49540 + "0.1": 5419, + "0.2": 12996, + "0.3": 24490, + "0.4": 36967, + "0.5": 49553 } }, { "pH": 10.0, "V": -0.5, "counts": { - "0.1": 7710, - "0.2": 22852, - "0.3": 40608, - "0.4": 54589, - "0.5": 65857 + "0.1": 7711, + "0.2": 22858, + "0.3": 40617, + "0.4": 54602, + "0.5": 65873 } }, { "pH": 10.0, "V": 0.0, "counts": { - "0.1": 8004, - "0.2": 24302, - "0.3": 42678, - "0.4": 55914, - "0.5": 63984 + "0.1": 8005, + "0.2": 24308, + "0.3": 42689, + "0.4": 55927, + "0.5": 63997 } }, { "pH": 10.0, "V": 0.5, "counts": { - "0.1": 7754, - "0.2": 23845, - "0.3": 41624, - "0.4": 53445, - "0.5": 61364 + "0.1": 7755, + "0.2": 23853, + "0.3": 41635, + "0.4": 53457, + "0.5": 61376 } }, { "pH": 10.0, "V": 1.0, "counts": { - "0.1": 6407, - "0.2": 18879, - "0.3": 32715, - "0.4": 43211, - "0.5": 52104 + "0.1": 6408, + "0.2": 18883, + "0.3": 32720, + "0.4": 43219, + "0.5": 52114 } }, { "pH": 10.0, "V": 1.5, "counts": { - "0.1": 5467, - "0.2": 14055, - "0.3": 23163, - "0.4": 31470, - "0.5": 39078 + "0.1": 5468, + "0.2": 14058, + "0.3": 23167, + "0.4": 31475, + "0.5": 39083 } }, { "pH": 10.0, "V": 2.0, "counts": { - "0.1": 5519, - "0.2": 10706, - "0.3": 16444, - "0.4": 22280, - "0.5": 27570 + "0.1": 5520, + "0.2": 10709, + "0.3": 16447, + "0.4": 22284, + "0.5": 27574 } }, { @@ -1130,97 +1130,97 @@ "counts": { "0.1": 4509, "0.2": 6382, - "0.3": 9122, - "0.4": 12474, - "0.5": 16116 + "0.3": 9123, + "0.4": 12475, + "0.5": 16118 } }, { "pH": 11.0, "V": -1.5, "counts": { - "0.1": 5274, - "0.2": 10022, - "0.3": 16490, - "0.4": 23641, - "0.5": 30801 + "0.1": 5276, + "0.2": 10024, + "0.3": 16492, + "0.4": 23645, + "0.5": 30810 } }, { "pH": 11.0, "V": -1.0, "counts": { - "0.1": 5594, - "0.2": 14147, - "0.3": 26912, - "0.4": 39766, - "0.5": 52315 + "0.1": 5595, + "0.2": 14150, + "0.3": 26920, + "0.4": 39777, + "0.5": 52327 } }, { "pH": 11.0, "V": -0.5, "counts": { - "0.1": 7896, - "0.2": 24285, - "0.3": 42104, - "0.4": 55889, - "0.5": 66673 + "0.1": 7897, + "0.2": 24291, + "0.3": 42114, + "0.4": 55904, + "0.5": 66688 } }, { "pH": 11.0, "V": 0.0, "counts": { - "0.1": 7671, - "0.2": 23883, - "0.3": 42596, - "0.4": 55424, - "0.5": 63301 + "0.1": 7672, + "0.2": 23891, + "0.3": 42607, + "0.4": 55436, + "0.5": 63314 } }, { "pH": 11.0, "V": 0.5, "counts": { - "0.1": 7308, - "0.2": 23416, - "0.3": 40677, - "0.4": 52087, - "0.5": 60345 + "0.1": 7309, + "0.2": 23423, + "0.3": 40688, + "0.4": 52099, + "0.5": 60357 } }, { "pH": 11.0, "V": 1.0, "counts": { - "0.1": 6285, - "0.2": 18400, - "0.3": 31809, - "0.4": 41814, - "0.5": 50400 + "0.1": 6286, + "0.2": 18403, + "0.3": 31814, + "0.4": 41822, + "0.5": 50409 } }, { "pH": 11.0, "V": 1.5, "counts": { - "0.1": 5233, - "0.2": 13070, - "0.3": 21888, - "0.4": 29652, - "0.5": 37242 + "0.1": 5234, + "0.2": 13073, + "0.3": 21891, + "0.4": 29657, + "0.5": 37247 } }, { "pH": 11.0, "V": 2.0, "counts": { - "0.1": 5335, - "0.2": 10235, - "0.3": 15692, - "0.4": 21382, - "0.5": 26574 + "0.1": 5336, + "0.2": 10238, + "0.3": 15695, + "0.4": 21385, + "0.5": 26578 } }, { @@ -1229,97 +1229,97 @@ "counts": { "0.1": 4480, "0.2": 6626, - "0.3": 9661, - "0.4": 13271, - "0.5": 17173 + "0.3": 9662, + "0.4": 13273, + "0.5": 17175 } }, { "pH": 12.0, "V": -1.5, "counts": { - "0.1": 5311, - "0.2": 10940, - "0.3": 17430, - "0.4": 24828, - "0.5": 32265 + "0.1": 5313, + "0.2": 10942, + "0.3": 17432, + "0.4": 24833, + "0.5": 32273 } }, { "pH": 12.0, "V": -1.0, "counts": { - "0.1": 5765, - "0.2": 15560, - "0.3": 29041, - "0.4": 42104, - "0.5": 54537 + "0.1": 5766, + "0.2": 15564, + "0.3": 29049, + "0.4": 42115, + "0.5": 54549 } }, { "pH": 12.0, "V": -0.5, "counts": { - "0.1": 7251, - "0.2": 23497, - "0.3": 41159, - "0.4": 55266, - "0.5": 65394 + "0.1": 7252, + "0.2": 23503, + "0.3": 41168, + "0.4": 55280, + "0.5": 65409 } }, { "pH": 12.0, "V": 0.0, "counts": { - "0.1": 7272, - "0.2": 23423, - "0.3": 41846, - "0.4": 54426, - "0.5": 62488 + "0.1": 7273, + "0.2": 23431, + "0.3": 41857, + "0.4": 54438, + "0.5": 62501 } }, { "pH": 12.0, "V": 0.5, "counts": { - "0.1": 6875, - "0.2": 22928, - "0.3": 39297, - "0.4": 50413, - "0.5": 58970 + "0.1": 6876, + "0.2": 22935, + "0.3": 39307, + "0.4": 50425, + "0.5": 58982 } }, { "pH": 12.0, "V": 1.0, "counts": { - "0.1": 5954, - "0.2": 17295, - "0.3": 30018, - "0.4": 40047, - "0.5": 48285 + "0.1": 5955, + "0.2": 17298, + "0.3": 30023, + "0.4": 40055, + "0.5": 48294 } }, { "pH": 12.0, "V": 1.5, "counts": { - "0.1": 5116, - "0.2": 12167, - "0.3": 20663, - "0.4": 27974, - "0.5": 35400 + "0.1": 5117, + "0.2": 12170, + "0.3": 20666, + "0.4": 27979, + "0.5": 35405 } }, { "pH": 12.0, "V": 2.0, "counts": { - "0.1": 5163, - "0.2": 9503, - "0.3": 14743, - "0.4": 20376, - "0.5": 25422 + "0.1": 5164, + "0.2": 9506, + "0.3": 14746, + "0.4": 20379, + "0.5": 25426 } }, { @@ -1327,87 +1327,87 @@ "V": -2.0, "counts": { "0.1": 4502, - "0.2": 7044, - "0.3": 10470, - "0.4": 14319, - "0.5": 18372 + "0.2": 7045, + "0.3": 10472, + "0.4": 14321, + "0.5": 18374 } }, { "pH": 13.0, "V": -1.5, "counts": { - "0.1": 5634, - "0.2": 11491, - "0.3": 18111, - "0.4": 25580, - "0.5": 33878 + "0.1": 5636, + "0.2": 11493, + "0.3": 18113, + "0.4": 25587, + "0.5": 33886 } }, { "pH": 13.0, "V": -1.0, "counts": { - "0.1": 5627, - "0.2": 16442, - "0.3": 30796, - "0.4": 43612, - "0.5": 55602 + "0.1": 5628, + "0.2": 16446, + "0.3": 30804, + "0.4": 43623, + "0.5": 55615 } }, { "pH": 13.0, "V": -0.5, "counts": { - "0.1": 6521, - "0.2": 21706, - "0.3": 39633, - "0.4": 54140, - "0.5": 63705 + "0.1": 6522, + "0.2": 21713, + "0.3": 39643, + "0.4": 54154, + "0.5": 63720 } }, { "pH": 13.0, "V": 0.0, "counts": { - "0.1": 6961, - "0.2": 22323, - "0.3": 40597, - "0.4": 53026, - "0.5": 61429 + "0.1": 6962, + "0.2": 22331, + "0.3": 40608, + "0.4": 53038, + "0.5": 61442 } }, { "pH": 13.0, "V": 0.5, "counts": { - "0.1": 6572, - "0.2": 21697, - "0.3": 37606, - "0.4": 48262, - "0.5": 56762 + "0.1": 6573, + "0.2": 21702, + "0.3": 37616, + "0.4": 48273, + "0.5": 56774 } }, { "pH": 13.0, "V": 1.0, "counts": { - "0.1": 5452, - "0.2": 15575, - "0.3": 27770, - "0.4": 37440, - "0.5": 45419 + "0.1": 5453, + "0.2": 15578, + "0.3": 27775, + "0.4": 37448, + "0.5": 45428 } }, { "pH": 13.0, "V": 1.5, "counts": { - "0.1": 4892, - "0.2": 11069, - "0.3": 19136, - "0.4": 25912, - "0.5": 33206 + "0.1": 4893, + "0.2": 11072, + "0.3": 19139, + "0.4": 25916, + "0.5": 33211 } }, { @@ -1415,10 +1415,10 @@ "V": 2.0, "counts": { "0.1": 5005, - "0.2": 8687, - "0.3": 13755, - "0.4": 19293, - "0.5": 24175 + "0.2": 8689, + "0.3": 13758, + "0.4": 19296, + "0.5": 24179 } }, { @@ -1426,87 +1426,87 @@ "V": -2.0, "counts": { "0.1": 4538, - "0.2": 7773, - "0.3": 11454, - "0.4": 15590, - "0.5": 20031 + "0.2": 7775, + "0.3": 11456, + "0.4": 15592, + "0.5": 20033 } }, { "pH": 14.0, "V": -1.5, "counts": { - "0.1": 5423, - "0.2": 11400, - "0.3": 18068, - "0.4": 25825, - "0.5": 35166 + "0.1": 5425, + "0.2": 11402, + "0.3": 18070, + "0.4": 25832, + "0.5": 35174 } }, { "pH": 14.0, "V": -1.0, "counts": { - "0.1": 5541, - "0.2": 16436, - "0.3": 30791, - "0.4": 43784, - "0.5": 56496 + "0.1": 5542, + "0.2": 16442, + "0.3": 30799, + "0.4": 43794, + "0.5": 56509 } }, { "pH": 14.0, "V": -0.5, "counts": { - "0.1": 6021, - "0.2": 19946, - "0.3": 37515, - "0.4": 52394, - "0.5": 62275 + "0.1": 6022, + "0.2": 19952, + "0.3": 37524, + "0.4": 52406, + "0.5": 62290 } }, { "pH": 14.0, "V": 0.0, "counts": { - "0.1": 6438, - "0.2": 20695, - "0.3": 38683, - "0.4": 50936, - "0.5": 59753 + "0.1": 6439, + "0.2": 20702, + "0.3": 38694, + "0.4": 50947, + "0.5": 59765 } }, { "pH": 14.0, "V": 0.5, "counts": { - "0.1": 5994, - "0.2": 20074, - "0.3": 35354, - "0.4": 45953, - "0.5": 54416 + "0.1": 5995, + "0.2": 20077, + "0.3": 35363, + "0.4": 45963, + "0.5": 54427 } }, { "pH": 14.0, "V": 1.0, "counts": { - "0.1": 4903, - "0.2": 13615, - "0.3": 25348, - "0.4": 34631, - "0.5": 42384 + "0.1": 4904, + "0.2": 13617, + "0.3": 25353, + "0.4": 34636, + "0.5": 42392 } }, { "pH": 14.0, "V": 1.5, "counts": { - "0.1": 4688, - "0.2": 10074, - "0.3": 17564, - "0.4": 24215, - "0.5": 30716 + "0.1": 4689, + "0.2": 10076, + "0.3": 17567, + "0.4": 24219, + "0.5": 30721 } }, { @@ -1514,11 +1514,11 @@ "V": 2.0, "counts": { "0.1": 4807, - "0.2": 8014, - "0.3": 12746, - "0.4": 17986, - "0.5": 22835 + "0.2": 8015, + "0.3": 12749, + "0.4": 17989, + "0.5": 22838 } } ] -} +} \ No newline at end of file diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index b72be260..8703963d 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -30,13 +30,6 @@ __author__ = "Leo Karlsson" -# Grid and display constants -# Height of the heatmap graph. A *definite* height (rather than height:100%) -# is important: the graph is embedded in the web app inside a dcc.Loading -# wrapper, which has no height of its own and so breaks any height:100% chain. -# With autosize and no definite height, each redraw (e.g. moving the stability -# cutoff) resolved to a different height and the white tile flickered between -# full height and ~450px. A fixed height makes every redraw deterministic. HEATMAP_HEIGHT = "70vh" # WIDTH = 700 MIN_PH = 0 @@ -50,13 +43,6 @@ CUTOFF_RANGE = [0.1, 0.5] CUTOFF_STEP = 0.1 -# Ion concentration options (mol/L) for the dummy dropdown. Materials Project -# default is 1e-6 M; the others span the practical range used in literature. -ION_CONCENTRATION_OPTIONS = [1e-8, 1e-6, 1e-4, 1e-2, 1.0] -DEFAULT_ION_CONCENTRATION = 1e-6 - -DEFAULT_FILTER_SOLIDS = True - class ReversePourbaixDiagramComponent(MPComponent): """Component for displaying a reverse Pourbaix diagram. @@ -69,8 +55,6 @@ class ReversePourbaixDiagramComponent(MPComponent): default_state = frozendict( show_water_lines=True, stability_cutoff=DEFAULT_CUTOFF, - filter_solids=DEFAULT_FILTER_SOLIDS, - ion_concentration=DEFAULT_ION_CONCENTRATION, ) default_plot_style = frozendict( @@ -111,9 +95,6 @@ class ReversePourbaixDiagramComponent(MPComponent): hovermode="closest", showlegend=False, margin=dict(l=80, b=70, t=10, r=20), - # height intentionally not set here: the height is fixed via the - # dcc.Graph CSS style (HEATMAP_HEIGHT) so Plotly's responsive autosize - # has a definite container height to fill on every redraw. ) empty_plot_style = frozendict( @@ -132,8 +113,6 @@ def __init__(self, parquet_path: str | Path | None = None, *args, **kwargs): Current parquet data is computed with solid filter and default ion concentrations. """ - # TODO: in future, it would be nice if user can disable solid filter and - # specify ion concentrations. super().__init__(*args, **kwargs) self._stability_df: pd.DataFrame | None = None if parquet_path is not None: @@ -285,11 +264,6 @@ def _sub_layouts(self) -> dict[str, Component]: "displaylogo": False, "responsive": True, }, - # Definite height (see HEATMAP_HEIGHT note above). This used - # to be height:100% + flex fill, which depends on a definite- - # height ancestor that the web app's dcc.Loading wrapper does - # not provide, so the autosize height was non-deterministic - # and the tile flickered on each cutoff redraw. style={"height": HEATMAP_HEIGHT, "width": "100%"}, ), ], @@ -339,37 +313,6 @@ def _sub_layouts(self) -> dict[str, Component]: "Higher cutoffs include progressively more metastable phases." ), ), - self.get_bool_input( - kwarg_label="filter_solids", - default=self.default_state["filter_solids"], - label="Filter Solid Phases", - help_str=( - "When enabled, only solid phases that are stable on the " - "compositional phase diagram are included in the Pourbaix " - "analysis. Highly oxidized or reduced phases that may appear " - "in experiments due to kinetic limitations on oxygen or " - "hydrogen evolution are excluded — these are not truly " - "thermodynamically stable and are often overstabilized by " - "DFT errors. Including only stable solid phases generally " - "yields the most accurate Pourbaix diagrams. " - "(Currently a placeholder — does not affect the diagram.)" - ), - ), - self.get_choice_input( - kwarg_label="ion_concentration", - default=self.default_state["ion_concentration"], - options=[ - {"label": f"{c:.0e} M", "value": c} - for c in ION_CONCENTRATION_OPTIONS - ], - label="Ion Concentration (mol/L)", - help_str=( - "Activity of dissolved ionic species used to construct the " - "Pourbaix diagram, applied uniformly to all elements. Lower " - "concentrations expand the stability regions of solid phases. " - "(Currently a placeholder — does not affect the diagram.)" - ), - ), ] ) From 46148dbf485735ea2dbe86ebbc566c6f0080c157 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:45:03 +0000 Subject: [PATCH 25/33] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- crystal_toolkit/components/reverse_pourbaix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 8703963d..134bae0d 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -355,4 +355,4 @@ def update_figure( show_water_lines=bool(show_water_lines), ) - return figure \ No newline at end of file + return figure From e5d9ff828a63ea3bc60ff3ef58d4ed967acbd60d Mon Sep 17 00:00:00 2001 From: zccalka Date: Wed, 17 Jun 2026 15:18:35 -0700 Subject: [PATCH 26/33] added preprocessing so component can handle .parquet instead of .json for heatmap data --- .../apps/examples/reverse_pourbaix_example.py | 6 +- .../examples/reverse_pourbaix_heatmap.json | 1524 ----------------- .../examples/reverse_pourbaix_heatmap.parquet | Bin 0 -> 6559 bytes .../components/reverse_pourbaix.py | 35 + 4 files changed, 37 insertions(+), 1528 deletions(-) delete mode 100644 crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json create mode 100644 crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py index edec3bd8..712ac54c 100755 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -6,7 +6,6 @@ from __future__ import annotations -import json from pathlib import Path import dash @@ -19,10 +18,9 @@ app = dash.Dash(assets_folder=SETTINGS.ASSETS_PATH) # Load pre-computed heatmap data -DATA_PATH = Path(__file__).parent / "reverse_pourbaix_heatmap.json" +DATA_PATH = Path(__file__).parent / "reverse_pourbaix_heatmap.parquet" -with open(DATA_PATH) as f: - heatmap_data = json.load(f) +heatmap_data = ReversePourbaixDiagramComponent.load_heatmap_data(DATA_PATH) # Build the heatmap figure directly — no component, no callbacks. figure = ReversePourbaixDiagramComponent.get_heatmap_figure(heatmap_data) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json deleted file mode 100644 index 1b9b7011..00000000 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.json +++ /dev/null @@ -1,1524 +0,0 @@ -{ - "ph_values": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14 - ], - "v_values": [ - 2.0, - 1.5, - 1.0, - 0.5, - 0.0, - -0.5, - -1.0, - -1.5, - -2.0 - ], - "cutoffs": [ - 0.1, - 0.2, - 0.3, - 0.4, - 0.5 - ], - "grid": [ - { - "pH": 0.0, - "V": -2.0, - "counts": { - "0.1": 4435, - "0.2": 5622, - "0.3": 6820, - "0.4": 8045, - "0.5": 9467 - } - }, - { - "pH": 0.0, - "V": -1.5, - "counts": { - "0.1": 3825, - "0.2": 5138, - "0.3": 6649, - "0.4": 8839, - "0.5": 11709 - } - }, - { - "pH": 0.0, - "V": -1.0, - "counts": { - "0.1": 3040, - "0.2": 4670, - "0.3": 7284, - "0.4": 11430, - "0.5": 17112 - } - }, - { - "pH": 0.0, - "V": -0.5, - "counts": { - "0.1": 2326, - "0.2": 4953, - "0.3": 9906, - "0.4": 16989, - "0.5": 26490 - } - }, - { - "pH": 0.0, - "V": 0.0, - "counts": { - "0.1": 2071, - "0.2": 7228, - "0.3": 18534, - "0.4": 31187, - "0.5": 43231 - } - }, - { - "pH": 0.0, - "V": 0.5, - "counts": { - "0.1": 1771, - "0.2": 8919, - "0.3": 22616, - "0.4": 35752, - "0.5": 45833 - } - }, - { - "pH": 0.0, - "V": 1.0, - "counts": { - "0.1": 2095, - "0.2": 9667, - "0.3": 24020, - "0.4": 37254, - "0.5": 47898 - } - }, - { - "pH": 0.0, - "V": 1.5, - "counts": { - "0.1": 2071, - "0.2": 9495, - "0.3": 22469, - "0.4": 34824, - "0.5": 46232 - } - }, - { - "pH": 0.0, - "V": 2.0, - "counts": { - "0.1": 2383, - "0.2": 8674, - "0.3": 19278, - "0.4": 28695, - "0.5": 37971 - } - }, - { - "pH": 1.0, - "V": -2.0, - "counts": { - "0.1": 4445, - "0.2": 5655, - "0.3": 6942, - "0.4": 8189, - "0.5": 9713 - } - }, - { - "pH": 1.0, - "V": -1.5, - "counts": { - "0.1": 3857, - "0.2": 5279, - "0.3": 6973, - "0.4": 9420, - "0.5": 12639 - } - }, - { - "pH": 1.0, - "V": -1.0, - "counts": { - "0.1": 3012, - "0.2": 4935, - "0.3": 8051, - "0.4": 12755, - "0.5": 19068 - } - }, - { - "pH": 1.0, - "V": -0.5, - "counts": { - "0.1": 2617, - "0.2": 6018, - "0.3": 12111, - "0.4": 21216, - "0.5": 32797 - } - }, - { - "pH": 1.0, - "V": 0.0, - "counts": { - "0.1": 2459, - "0.2": 9181, - "0.3": 22269, - "0.4": 36258, - "0.5": 47974 - } - }, - { - "pH": 1.0, - "V": 0.5, - "counts": { - "0.1": 2491, - "0.2": 11385, - "0.3": 26453, - "0.4": 38617, - "0.5": 48812 - } - }, - { - "pH": 1.0, - "V": 1.0, - "counts": { - "0.1": 2815, - "0.2": 12000, - "0.3": 27788, - "0.4": 40651, - "0.5": 52154 - } - }, - { - "pH": 1.0, - "V": 1.5, - "counts": { - "0.1": 2730, - "0.2": 11768, - "0.3": 24943, - "0.4": 37647, - "0.5": 47754 - } - }, - { - "pH": 1.0, - "V": 2.0, - "counts": { - "0.1": 2951, - "0.2": 9975, - "0.3": 20420, - "0.4": 29596, - "0.5": 38461 - } - }, - { - "pH": 2.0, - "V": -2.0, - "counts": { - "0.1": 4449, - "0.2": 5715, - "0.3": 7050, - "0.4": 8368, - "0.5": 10004 - } - }, - { - "pH": 2.0, - "V": -1.5, - "counts": { - "0.1": 3902, - "0.2": 5503, - "0.3": 7457, - "0.4": 10211, - "0.5": 13818 - } - }, - { - "pH": 2.0, - "V": -1.0, - "counts": { - "0.1": 3044, - "0.2": 5353, - "0.3": 9140, - "0.4": 14583, - "0.5": 21474 - } - }, - { - "pH": 2.0, - "V": -0.5, - "counts": { - "0.1": 3068, - "0.2": 7325, - "0.3": 15545, - "0.4": 27231, - "0.5": 38250 - } - }, - { - "pH": 2.0, - "V": 0.0, - "counts": { - "0.1": 2794, - "0.2": 11491, - "0.3": 27040, - "0.4": 41045, - "0.5": 51247 - } - }, - { - "pH": 2.0, - "V": 0.5, - "counts": { - "0.1": 3406, - "0.2": 14411, - "0.3": 30009, - "0.4": 41943, - "0.5": 51791 - } - }, - { - "pH": 2.0, - "V": 1.0, - "counts": { - "0.1": 3523, - "0.2": 14766, - "0.3": 31045, - "0.4": 43830, - "0.5": 54663 - } - }, - { - "pH": 2.0, - "V": 1.5, - "counts": { - "0.1": 3447, - "0.2": 13323, - "0.3": 26170, - "0.4": 38531, - "0.5": 48246 - } - }, - { - "pH": 2.0, - "V": 2.0, - "counts": { - "0.1": 3197, - "0.2": 10401, - "0.3": 20948, - "0.4": 29689, - "0.5": 38231 - } - }, - { - "pH": 3.0, - "V": -2.0, - "counts": { - "0.1": 4458, - "0.2": 5789, - "0.3": 7165, - "0.4": 8551, - "0.5": 10418 - } - }, - { - "pH": 3.0, - "V": -1.5, - "counts": { - "0.1": 4000, - "0.2": 5807, - "0.3": 8041, - "0.4": 11400, - "0.5": 15061 - } - }, - { - "pH": 3.0, - "V": -1.0, - "counts": { - "0.1": 3171, - "0.2": 5950, - "0.3": 10481, - "0.4": 16504, - "0.5": 23618 - } - }, - { - "pH": 3.0, - "V": -0.5, - "counts": { - "0.1": 3758, - "0.2": 10029, - "0.3": 21326, - "0.4": 32603, - "0.5": 43314 - } - }, - { - "pH": 3.0, - "V": 0.0, - "counts": { - "0.1": 3414, - "0.2": 15035, - "0.3": 32239, - "0.4": 44658, - "0.5": 54396 - } - }, - { - "pH": 3.0, - "V": 0.5, - "counts": { - "0.1": 4431, - "0.2": 17575, - "0.3": 33006, - "0.4": 45258, - "0.5": 55598 - } - }, - { - "pH": 3.0, - "V": 1.0, - "counts": { - "0.1": 4288, - "0.2": 17484, - "0.3": 33597, - "0.4": 47048, - "0.5": 56402 - } - }, - { - "pH": 3.0, - "V": 1.5, - "counts": { - "0.1": 4103, - "0.2": 14470, - "0.3": 26881, - "0.4": 38723, - "0.5": 48174 - } - }, - { - "pH": 3.0, - "V": 2.0, - "counts": { - "0.1": 3359, - "0.2": 10562, - "0.3": 20508, - "0.4": 28926, - "0.5": 37015 - } - }, - { - "pH": 4.0, - "V": -2.0, - "counts": { - "0.1": 4474, - "0.2": 5854, - "0.3": 7253, - "0.4": 8830, - "0.5": 10754 - } - }, - { - "pH": 4.0, - "V": -1.5, - "counts": { - "0.1": 4108, - "0.2": 6137, - "0.3": 8886, - "0.4": 12398, - "0.5": 16510 - } - }, - { - "pH": 4.0, - "V": -1.0, - "counts": { - "0.1": 3336, - "0.2": 6794, - "0.3": 11907, - "0.4": 18243, - "0.5": 25593 - } - }, - { - "pH": 4.0, - "V": -0.5, - "counts": { - "0.1": 4380, - "0.2": 12918, - "0.3": 25076, - "0.4": 36275, - "0.5": 47278 - } - }, - { - "pH": 4.0, - "V": 0.0, - "counts": { - "0.1": 4439, - "0.2": 18894, - "0.3": 35721, - "0.4": 47503, - "0.5": 57235 - } - }, - { - "pH": 4.0, - "V": 0.5, - "counts": { - "0.1": 5219, - "0.2": 19356, - "0.3": 35462, - "0.4": 47774, - "0.5": 58538 - } - }, - { - "pH": 4.0, - "V": 1.0, - "counts": { - "0.1": 4945, - "0.2": 18664, - "0.3": 35053, - "0.4": 47895, - "0.5": 57061 - } - }, - { - "pH": 4.0, - "V": 1.5, - "counts": { - "0.1": 4496, - "0.2": 15325, - "0.3": 27283, - "0.4": 38499, - "0.5": 47339 - } - }, - { - "pH": 4.0, - "V": 2.0, - "counts": { - "0.1": 3815, - "0.2": 10804, - "0.3": 20361, - "0.4": 27881, - "0.5": 35691 - } - }, - { - "pH": 5.0, - "V": -2.0, - "counts": { - "0.1": 4482, - "0.2": 5908, - "0.3": 7382, - "0.4": 9107, - "0.5": 11145 - } - }, - { - "pH": 5.0, - "V": -1.5, - "counts": { - "0.1": 4270, - "0.2": 6622, - "0.3": 9698, - "0.4": 13628, - "0.5": 17979 - } - }, - { - "pH": 5.0, - "V": -1.0, - "counts": { - "0.1": 3697, - "0.2": 7778, - "0.3": 13221, - "0.4": 19792, - "0.5": 27826 - } - }, - { - "pH": 5.0, - "V": -0.5, - "counts": { - "0.1": 4980, - "0.2": 15349, - "0.3": 28249, - "0.4": 40093, - "0.5": 50613 - } - }, - { - "pH": 5.0, - "V": 0.0, - "counts": { - "0.1": 5423, - "0.2": 21163, - "0.3": 37281, - "0.4": 48889, - "0.5": 59007 - } - }, - { - "pH": 5.0, - "V": 0.5, - "counts": { - "0.1": 5945, - "0.2": 20665, - "0.3": 37567, - "0.4": 51067, - "0.5": 60422 - } - }, - { - "pH": 5.0, - "V": 1.0, - "counts": { - "0.1": 5456, - "0.2": 19302, - "0.3": 35333, - "0.4": 48127, - "0.5": 56742 - } - }, - { - "pH": 5.0, - "V": 1.5, - "counts": { - "0.1": 4968, - "0.2": 16092, - "0.3": 27604, - "0.4": 38101, - "0.5": 46697 - } - }, - { - "pH": 5.0, - "V": 2.0, - "counts": { - "0.1": 4257, - "0.2": 10989, - "0.3": 19919, - "0.4": 26928, - "0.5": 34271 - } - }, - { - "pH": 6.0, - "V": -2.0, - "counts": { - "0.1": 4497, - "0.2": 5987, - "0.3": 7580, - "0.4": 9424, - "0.5": 11591 - } - }, - { - "pH": 6.0, - "V": -1.5, - "counts": { - "0.1": 4371, - "0.2": 7209, - "0.3": 10590, - "0.4": 14775, - "0.5": 19634 - } - }, - { - "pH": 6.0, - "V": -1.0, - "counts": { - "0.1": 3931, - "0.2": 8540, - "0.3": 14303, - "0.4": 21231, - "0.5": 30362 - } - }, - { - "pH": 6.0, - "V": -0.5, - "counts": { - "0.1": 5554, - "0.2": 17450, - "0.3": 31227, - "0.4": 43293, - "0.5": 54016 - } - }, - { - "pH": 6.0, - "V": 0.0, - "counts": { - "0.1": 6509, - "0.2": 23325, - "0.3": 38953, - "0.4": 50591, - "0.5": 61890 - } - }, - { - "pH": 6.0, - "V": 0.5, - "counts": { - "0.1": 6611, - "0.2": 22344, - "0.3": 39764, - "0.4": 53044, - "0.5": 61892 - } - }, - { - "pH": 6.0, - "V": 1.0, - "counts": { - "0.1": 5710, - "0.2": 19683, - "0.3": 35413, - "0.4": 47925, - "0.5": 56396 - } - }, - { - "pH": 6.0, - "V": 1.5, - "counts": { - "0.1": 5410, - "0.2": 16908, - "0.3": 27897, - "0.4": 37364, - "0.5": 45960 - } - }, - { - "pH": 6.0, - "V": 2.0, - "counts": { - "0.1": 4651, - "0.2": 11131, - "0.3": 19230, - "0.4": 25925, - "0.5": 32782 - } - }, - { - "pH": 7.0, - "V": -2.0, - "counts": { - "0.1": 4500, - "0.2": 6110, - "0.3": 7758, - "0.4": 9785, - "0.5": 12268 - } - }, - { - "pH": 7.0, - "V": -1.5, - "counts": { - "0.1": 4676, - "0.2": 7789, - "0.3": 11561, - "0.4": 16152, - "0.5": 22056 - } - }, - { - "pH": 7.0, - "V": -1.0, - "counts": { - "0.1": 4140, - "0.2": 9038, - "0.3": 15384, - "0.4": 23253, - "0.5": 34168 - } - }, - { - "pH": 7.0, - "V": -0.5, - "counts": { - "0.1": 6165, - "0.2": 19242, - "0.3": 33716, - "0.4": 45957, - "0.5": 57747 - } - }, - { - "pH": 7.0, - "V": 0.0, - "counts": { - "0.1": 7342, - "0.2": 24268, - "0.3": 40601, - "0.4": 53258, - "0.5": 63720 - } - }, - { - "pH": 7.0, - "V": 0.5, - "counts": { - "0.1": 7328, - "0.2": 23922, - "0.3": 42292, - "0.4": 54238, - "0.5": 62510 - } - }, - { - "pH": 7.0, - "V": 1.0, - "counts": { - "0.1": 5938, - "0.2": 19795, - "0.3": 35510, - "0.4": 47358, - "0.5": 55990 - } - }, - { - "pH": 7.0, - "V": 1.5, - "counts": { - "0.1": 5274, - "0.2": 16249, - "0.3": 26897, - "0.4": 35903, - "0.5": 44727 - } - }, - { - "pH": 7.0, - "V": 2.0, - "counts": { - "0.1": 5018, - "0.2": 11223, - "0.3": 18633, - "0.4": 25008, - "0.5": 31347 - } - }, - { - "pH": 8.0, - "V": -2.0, - "counts": { - "0.1": 4540, - "0.2": 6247, - "0.3": 7997, - "0.4": 10194, - "0.5": 13206 - } - }, - { - "pH": 8.0, - "V": -1.5, - "counts": { - "0.1": 4937, - "0.2": 8422, - "0.3": 12684, - "0.4": 17918, - "0.5": 24872 - } - }, - { - "pH": 8.0, - "V": -1.0, - "counts": { - "0.1": 4478, - "0.2": 9787, - "0.3": 16891, - "0.4": 26928, - "0.5": 39468 - } - }, - { - "pH": 8.0, - "V": -0.5, - "counts": { - "0.1": 6531, - "0.2": 20403, - "0.3": 35643, - "0.4": 49078, - "0.5": 61086 - } - }, - { - "pH": 8.0, - "V": 0.0, - "counts": { - "0.1": 7462, - "0.2": 24285, - "0.3": 41239, - "0.4": 55249, - "0.5": 64264 - } - }, - { - "pH": 8.0, - "V": 0.5, - "counts": { - "0.1": 7512, - "0.2": 24118, - "0.3": 42601, - "0.4": 54643, - "0.5": 62497 - } - }, - { - "pH": 8.0, - "V": 1.0, - "counts": { - "0.1": 5942, - "0.2": 19522, - "0.3": 34636, - "0.4": 46248, - "0.5": 55009 - } - }, - { - "pH": 8.0, - "V": 1.5, - "counts": { - "0.1": 5273, - "0.2": 15487, - "0.3": 25589, - "0.4": 34489, - "0.5": 43169 - } - }, - { - "pH": 8.0, - "V": 2.0, - "counts": { - "0.1": 5230, - "0.2": 11124, - "0.3": 17868, - "0.4": 24052, - "0.5": 29944 - } - }, - { - "pH": 9.0, - "V": -2.0, - "counts": { - "0.1": 4613, - "0.2": 6376, - "0.3": 8271, - "0.4": 10981, - "0.5": 14131 - } - }, - { - "pH": 9.0, - "V": -1.5, - "counts": { - "0.1": 5249, - "0.2": 9085, - "0.3": 13723, - "0.4": 20197, - "0.5": 27228 - } - }, - { - "pH": 9.0, - "V": -1.0, - "counts": { - "0.1": 4843, - "0.2": 10839, - "0.3": 19839, - "0.4": 31882, - "0.5": 44689 - } - }, - { - "pH": 9.0, - "V": -0.5, - "counts": { - "0.1": 7170, - "0.2": 21517, - "0.3": 38030, - "0.4": 52132, - "0.5": 63941 - } - }, - { - "pH": 9.0, - "V": 0.0, - "counts": { - "0.1": 7792, - "0.2": 24238, - "0.3": 42191, - "0.4": 56132, - "0.5": 64357 - } - }, - { - "pH": 9.0, - "V": 0.5, - "counts": { - "0.1": 7777, - "0.2": 24097, - "0.3": 42329, - "0.4": 54315, - "0.5": 62087 - } - }, - { - "pH": 9.0, - "V": 1.0, - "counts": { - "0.1": 6258, - "0.2": 19286, - "0.3": 33633, - "0.4": 44767, - "0.5": 53717 - } - }, - { - "pH": 9.0, - "V": 1.5, - "counts": { - "0.1": 5464, - "0.2": 14925, - "0.3": 24472, - "0.4": 33106, - "0.5": 41371 - } - }, - { - "pH": 9.0, - "V": 2.0, - "counts": { - "0.1": 5395, - "0.2": 10951, - "0.3": 17122, - "0.4": 23138, - "0.5": 28686 - } - }, - { - "pH": 10.0, - "V": -2.0, - "counts": { - "0.1": 4576, - "0.2": 6346, - "0.3": 8572, - "0.4": 11642, - "0.5": 15174 - } - }, - { - "pH": 10.0, - "V": -1.5, - "counts": { - "0.1": 5387, - "0.2": 9435, - "0.3": 15110, - "0.4": 22147, - "0.5": 29141 - } - }, - { - "pH": 10.0, - "V": -1.0, - "counts": { - "0.1": 5419, - "0.2": 12996, - "0.3": 24490, - "0.4": 36967, - "0.5": 49553 - } - }, - { - "pH": 10.0, - "V": -0.5, - "counts": { - "0.1": 7711, - "0.2": 22858, - "0.3": 40617, - "0.4": 54602, - "0.5": 65873 - } - }, - { - "pH": 10.0, - "V": 0.0, - "counts": { - "0.1": 8005, - "0.2": 24308, - "0.3": 42689, - "0.4": 55927, - "0.5": 63997 - } - }, - { - "pH": 10.0, - "V": 0.5, - "counts": { - "0.1": 7755, - "0.2": 23853, - "0.3": 41635, - "0.4": 53457, - "0.5": 61376 - } - }, - { - "pH": 10.0, - "V": 1.0, - "counts": { - "0.1": 6408, - "0.2": 18883, - "0.3": 32720, - "0.4": 43219, - "0.5": 52114 - } - }, - { - "pH": 10.0, - "V": 1.5, - "counts": { - "0.1": 5468, - "0.2": 14058, - "0.3": 23167, - "0.4": 31475, - "0.5": 39083 - } - }, - { - "pH": 10.0, - "V": 2.0, - "counts": { - "0.1": 5520, - "0.2": 10709, - "0.3": 16447, - "0.4": 22284, - "0.5": 27574 - } - }, - { - "pH": 11.0, - "V": -2.0, - "counts": { - "0.1": 4509, - "0.2": 6382, - "0.3": 9123, - "0.4": 12475, - "0.5": 16118 - } - }, - { - "pH": 11.0, - "V": -1.5, - "counts": { - "0.1": 5276, - "0.2": 10024, - "0.3": 16492, - "0.4": 23645, - "0.5": 30810 - } - }, - { - "pH": 11.0, - "V": -1.0, - "counts": { - "0.1": 5595, - "0.2": 14150, - "0.3": 26920, - "0.4": 39777, - "0.5": 52327 - } - }, - { - "pH": 11.0, - "V": -0.5, - "counts": { - "0.1": 7897, - "0.2": 24291, - "0.3": 42114, - "0.4": 55904, - "0.5": 66688 - } - }, - { - "pH": 11.0, - "V": 0.0, - "counts": { - "0.1": 7672, - "0.2": 23891, - "0.3": 42607, - "0.4": 55436, - "0.5": 63314 - } - }, - { - "pH": 11.0, - "V": 0.5, - "counts": { - "0.1": 7309, - "0.2": 23423, - "0.3": 40688, - "0.4": 52099, - "0.5": 60357 - } - }, - { - "pH": 11.0, - "V": 1.0, - "counts": { - "0.1": 6286, - "0.2": 18403, - "0.3": 31814, - "0.4": 41822, - "0.5": 50409 - } - }, - { - "pH": 11.0, - "V": 1.5, - "counts": { - "0.1": 5234, - "0.2": 13073, - "0.3": 21891, - "0.4": 29657, - "0.5": 37247 - } - }, - { - "pH": 11.0, - "V": 2.0, - "counts": { - "0.1": 5336, - "0.2": 10238, - "0.3": 15695, - "0.4": 21385, - "0.5": 26578 - } - }, - { - "pH": 12.0, - "V": -2.0, - "counts": { - "0.1": 4480, - "0.2": 6626, - "0.3": 9662, - "0.4": 13273, - "0.5": 17175 - } - }, - { - "pH": 12.0, - "V": -1.5, - "counts": { - "0.1": 5313, - "0.2": 10942, - "0.3": 17432, - "0.4": 24833, - "0.5": 32273 - } - }, - { - "pH": 12.0, - "V": -1.0, - "counts": { - "0.1": 5766, - "0.2": 15564, - "0.3": 29049, - "0.4": 42115, - "0.5": 54549 - } - }, - { - "pH": 12.0, - "V": -0.5, - "counts": { - "0.1": 7252, - "0.2": 23503, - "0.3": 41168, - "0.4": 55280, - "0.5": 65409 - } - }, - { - "pH": 12.0, - "V": 0.0, - "counts": { - "0.1": 7273, - "0.2": 23431, - "0.3": 41857, - "0.4": 54438, - "0.5": 62501 - } - }, - { - "pH": 12.0, - "V": 0.5, - "counts": { - "0.1": 6876, - "0.2": 22935, - "0.3": 39307, - "0.4": 50425, - "0.5": 58982 - } - }, - { - "pH": 12.0, - "V": 1.0, - "counts": { - "0.1": 5955, - "0.2": 17298, - "0.3": 30023, - "0.4": 40055, - "0.5": 48294 - } - }, - { - "pH": 12.0, - "V": 1.5, - "counts": { - "0.1": 5117, - "0.2": 12170, - "0.3": 20666, - "0.4": 27979, - "0.5": 35405 - } - }, - { - "pH": 12.0, - "V": 2.0, - "counts": { - "0.1": 5164, - "0.2": 9506, - "0.3": 14746, - "0.4": 20379, - "0.5": 25426 - } - }, - { - "pH": 13.0, - "V": -2.0, - "counts": { - "0.1": 4502, - "0.2": 7045, - "0.3": 10472, - "0.4": 14321, - "0.5": 18374 - } - }, - { - "pH": 13.0, - "V": -1.5, - "counts": { - "0.1": 5636, - "0.2": 11493, - "0.3": 18113, - "0.4": 25587, - "0.5": 33886 - } - }, - { - "pH": 13.0, - "V": -1.0, - "counts": { - "0.1": 5628, - "0.2": 16446, - "0.3": 30804, - "0.4": 43623, - "0.5": 55615 - } - }, - { - "pH": 13.0, - "V": -0.5, - "counts": { - "0.1": 6522, - "0.2": 21713, - "0.3": 39643, - "0.4": 54154, - "0.5": 63720 - } - }, - { - "pH": 13.0, - "V": 0.0, - "counts": { - "0.1": 6962, - "0.2": 22331, - "0.3": 40608, - "0.4": 53038, - "0.5": 61442 - } - }, - { - "pH": 13.0, - "V": 0.5, - "counts": { - "0.1": 6573, - "0.2": 21702, - "0.3": 37616, - "0.4": 48273, - "0.5": 56774 - } - }, - { - "pH": 13.0, - "V": 1.0, - "counts": { - "0.1": 5453, - "0.2": 15578, - "0.3": 27775, - "0.4": 37448, - "0.5": 45428 - } - }, - { - "pH": 13.0, - "V": 1.5, - "counts": { - "0.1": 4893, - "0.2": 11072, - "0.3": 19139, - "0.4": 25916, - "0.5": 33211 - } - }, - { - "pH": 13.0, - "V": 2.0, - "counts": { - "0.1": 5005, - "0.2": 8689, - "0.3": 13758, - "0.4": 19296, - "0.5": 24179 - } - }, - { - "pH": 14.0, - "V": -2.0, - "counts": { - "0.1": 4538, - "0.2": 7775, - "0.3": 11456, - "0.4": 15592, - "0.5": 20033 - } - }, - { - "pH": 14.0, - "V": -1.5, - "counts": { - "0.1": 5425, - "0.2": 11402, - "0.3": 18070, - "0.4": 25832, - "0.5": 35174 - } - }, - { - "pH": 14.0, - "V": -1.0, - "counts": { - "0.1": 5542, - "0.2": 16442, - "0.3": 30799, - "0.4": 43794, - "0.5": 56509 - } - }, - { - "pH": 14.0, - "V": -0.5, - "counts": { - "0.1": 6022, - "0.2": 19952, - "0.3": 37524, - "0.4": 52406, - "0.5": 62290 - } - }, - { - "pH": 14.0, - "V": 0.0, - "counts": { - "0.1": 6439, - "0.2": 20702, - "0.3": 38694, - "0.4": 50947, - "0.5": 59765 - } - }, - { - "pH": 14.0, - "V": 0.5, - "counts": { - "0.1": 5995, - "0.2": 20077, - "0.3": 35363, - "0.4": 45963, - "0.5": 54427 - } - }, - { - "pH": 14.0, - "V": 1.0, - "counts": { - "0.1": 4904, - "0.2": 13617, - "0.3": 25353, - "0.4": 34636, - "0.5": 42392 - } - }, - { - "pH": 14.0, - "V": 1.5, - "counts": { - "0.1": 4689, - "0.2": 10076, - "0.3": 17567, - "0.4": 24219, - "0.5": 30721 - } - }, - { - "pH": 14.0, - "V": 2.0, - "counts": { - "0.1": 4807, - "0.2": 8015, - "0.3": 12749, - "0.4": 17989, - "0.5": 22838 - } - } - ] -} \ No newline at end of file diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet new file mode 100644 index 0000000000000000000000000000000000000000..876fe89a2fbe56d87e3cfe02fc59078959ba9a44 GIT binary patch literal 6559 zcmcgx30PBS);<9Yi=u2nu!tBn;sOEz6kH%|A?#~H!kP^TCV&u@U=1J8EmO3tDUIN*C)wb=s+RwAeaRt*xE+CMdKz^Z)A=JeTc>^N)=+s(!5(bA(cTH#EdPm$W&LzCBSj`uzrmW7OD zESZ_iTxKB~C1cAhWmYn4nT>3;Y>doSHWtX+S)7bGj)M_+G?6undaW%j?!67%gTOUl zYh}-}7znYpVJ@fM3o+p!2I8BeI6GKR0ZE~cLG})V94R$|ykhp*Bb2p7cJ}sMt{&6q48_x8%VrkK;!udSmDL9!{821AZ)-R8^C9>HA%Nd$!O3_E`hgG^t1X+E*P)PM z^dqH4^vz~jL+H(Go1!A7?6N=GR2bpUonYVga2(<9;5Z_$hkqsu76a|x49=>Vah#(j zbU!=YU22nxO#PEX^!x-O-6Wz1Q;F`3C30{gy6;NV$R&Eqi)dgp(IS+U-i>I_Y@*M? zh@7p7q>eaJ26D7oAt&}LW8-9RyyfcyeGeo}t?;Y&1N083} z??T8<0e`M9(R;uv1h#VMoX7REF+@Q&M61Tb?i|FELNsv&(N_4lJO#GIM7!F68#so8BRJ5MxCj4Fpz8(!Q_JGS1;K-IJ7JGs!!1Zp#coO_Vwl9b%5_t%G8QA)XsvsNf zPZXR_R0nL8ZxSsVL$uQqxkyINfms3{MZmlrn12E00_5N|toH!(pCHqK(z|2kX_LL}6L*iTd$EObNiBS&KRJ5|J2wk3j#sWkhBBkdF(%2p@llAi9k_oZL_3 z{W(#u9Z?))3+sqZyg`(5iO3SN2E@ifyx|+62h5XTFKh;pTN2U3QlgFRs6W`d0(|Li zM9WccW;2P#VDEk4JBaIIAIO2x3VT~7Ab#XCT!?ig>|k%DAJJXp@<1D6_zQS|>|+s8 zb1TuhLquIy5ijzyEf#oMh;|%C4g8fT*^%g*Xrfz9s7=(*S67JIvG?+P>?y(?@cb?8 z{SCbM15-0<5_o)hqW0?b{=d-E*#2IecS$Uka(K>*SGD553Ut8gcJflrha zk9=Ui9)AA;AJSsvY8%m?-$yNQh+al*@9aQasN34FQ4_F#D2eFpPNJp9iP(RK4d~?~ zj!}r?S76?AnWzx`W311z_I`80g`fd8xYL><7agw1WJ-81Mnp%;-O zWHS&$4s!BF5b(tz_o$aF)Xvqp$U87?se~MUXCl834Cud?2+fJPgIFhKV*e)eF=BY< z4#bc}ONi38g0s`;C&&_z+v~8K4$J|-9CwL`3wvWQd+vjeyB(;@14OeN5GQzaM|_cL zqOqv2Zs5BD%n9Hy2>to@IYbfA)8Tps@VkT%d5VcAn|n|Bb|oI(EYp+6ui%Ld*T;0tki-$m`iXDYZm2+R*r0|zgH2h@-O z{_MeL$Qtmq4>CveEiiw8I@*sKEUzJY6POF&d*LLabLgiHd?FF_zV^Tz@`b%fV218; z=*>q=4+Ox!1$7PX4xq;7x5D4ssMFiH86#Jhfp`2)qO+f(2OeM^Lly{`W)J-JV-^8# zAn=BPeq?B`~)!qZ!P%v2m0ht18U+$^b%xx;5`KlcMRP&kAL2f6pQzLwJOkh9 z=aH)~h}6I=hio!rQgA%$0%qFx;2b{1u~q6K>&T@8RhP?;w;nyB@S(gsm<=E4KX9LI{0!!jT%(GO~ZUOuN z^EJeLbQ<)aKRz5BLGLK?;Szz_E{C1vn5)jHsRif{a9#(DoPNvzU^xveXL^9=Q`FS2 zs9DGwAzKL?doB=7yNlU10rvsqqjfXp0kGwNjrl$v+`^Zz0{Pem-8YdFN7Nnq|8Np= zqeGpvpl^VM3v640P%G%26eao%nCqWGO?%@01|Itmy9MxlH5=HYkR#~itj0Z>gL@!& zPXp%#dy%WpQA@7KbsBQ81-{N>4#3WQ31$TN^gfMxA0&DOI(^U?>_j~QSKnn|fvt-R zz@rxP4|4shn2o?2It%p@gZqIRIbVtVKz0W;do%!fj79$T&AxlJWeB_In3@m|C zfXvuen`>$1w}1w7g@ZTvR>B*lzlPW0oGPJMfyKuYY)%HSC8zdKbNjS*f#u}x_OQ{u7uv(e37k429Fx;JBFD?SJEERha-l;wX{A$V z^puXYPSNz8-JLPBUcJy6JLi~FR~-LrT37tsFT1-E=HI!{l^DX}bSH(|r*|g{C$H~L ziS_O8PE8bW)~BT=r>{?!$k(qIXD;bqFUeiW*^r^^NZ%kW*tvd#Z1Jo88!}6daW-a^ zdqqgHD=*hea%yh9FUej0%czXJRkl$X@`i~G8HyIKz6|Bsg>0#+Gd@bH-jLlO&EKNy zlNRh)#+DWCYKxL-_H1vE743bcPquj9TkK5jYac~r>JDCR$Sgj5t1q+U=r8Q7(syiy zSxfpRHfEKb^g5nZ{_#S~Z2h@-VRpqQ*^Swi7j?(8tFA1w%&ETCCd@H>y}dD~=H@HM zbC%wI%QE-b?>`deF7x{5<;L9QKixW>Tl?@A%e)n4W25s{vfZ2VR@r!;$g3N>$V$H2 zAt72`@0`;lZ3AE>$#7FnyiQxZh#_2Qgn^@bG9p=lelYwl0mByS1g zZB}nF@V0379^!4)9lp=ow&Y~c^z9WF4byiRZXTNc+_InUPw!c26YROOenO4suI8DC zJ$JVS|K$06M@;Zb@@{d_jTbiM@4c~STjfpMVwe}l!Zj;^T-fExLbXoLu_BJmG&2{o z(rD8OOKkdND=YLR`T4{#!|EtyNky@qocnBC-CW~%9TRLwt=Y4O^``PTVpON4A zhrdaWlwz{{FZ~RKZF07P?R?OTH^}C75C1YU@(cQ~vysyO*_qi8c_vy-z7EcFJ~@wf z+mCk=1dbSrPmIX_5Qy`N7^H${<-A6KcwN|~@?w>|+~os{rCd$1N?o0+EYVizip#md z9+}y*J-DXJT;rPhcT(f!Qf{$ar-nMWG@6SI`5LuWmHS(n$w{u>P-;}n*Otijf&Pqk zafPnb@U#V;S}#}0^>P?4uFz_i&Gr~}B!1G5QS@JSW2){aeGSY1+YXrydWyeC;@`Q` z6zlnZPm=gp{Cf(SbjtPmGL51_Z%pMqzj$ruTfG9S3c z&c)CVn=8_3D`Y~ke}-Q|r6O9aR}{x<5;b9pjMSKI)7x)0$3JuJDWUNhnIz#Dxnqiej-|8LiD%MQf{u?CKQ4ph8uI)W>K; z=c`o-qZUguQVJQ{&@Wd+16%ZOut-e0vQn9(dU1wdK%qjCju-_RvBrq8N-7C}jfDT; zC%{*c5f+faPYF;8O?zd!>Pl%wvR{HJPQ45mWWp#PDHFdC`oij!f2x?z9K%{zeZH- z!*qp^c~c6b&=XNLI7NR61ZB)yH1lTc9MgMr8k%+-Mhor~Q5cDvON7pzv{F3 str: """ return f"{cutoff:.1f}" + @staticmethod + def load_heatmap_data(path: str | Path) -> dict[str, Any]: + """Load heatmap counts from a tidy, long-format parquet (one row per + pH/V/cutoff combination, columns "pH", "V", "cutoff", "count") and + reshape into the dict `get_heatmap_figure` expects: + {"ph_values", "v_values", "cutoffs", "grid"}, where `grid` is a list + of {"pH", "V", "counts": {cutoff_str: count}}. + """ + df = pd.read_parquet(path) + + ph_values = sorted(df["pH"].unique().tolist()) + v_values = sorted(df["V"].unique().tolist(), reverse=True) + cutoffs = sorted(df["cutoff"].unique().tolist()) + + grid = [ + { + "pH": ph, + "V": v, + "counts": { + ReversePourbaixDiagramComponent._format_cutoff_key( + float(cutoff) + ): int(count) + for cutoff, count in zip(group["cutoff"], group["count"]) + }, + } + for (ph, v), group in df.groupby(["pH", "V"], sort=True) + ] + + return { + "ph_values": ph_values, + "v_values": v_values, + "cutoffs": cutoffs, + "grid": grid, + } + def get_stable_mp_ids(self, ph: float, v: float, cutoff: float) -> list[str]: """Return mp_ids stable at (pH, V) below the given decomposition-energy cutoff. From d6eb3ab3155d16e0524360f36677744b7b56f505 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 18 Jun 2026 12:07:09 -0700 Subject: [PATCH 27/33] changes to handle S3 instead of local files --- .../apps/examples/reverse_pourbaix_example.py | 13 ++++--- .../examples/reverse_pourbaix_heatmap.parquet | Bin 6559 -> 0 bytes .../components/reverse_pourbaix.py | 35 +++++++++++++----- 3 files changed, 34 insertions(+), 14 deletions(-) delete mode 100644 crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py index 712ac54c..f6dd2bde 100755 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -6,8 +6,6 @@ from __future__ import annotations -from pathlib import Path - import dash from dash import dcc, html @@ -17,10 +15,15 @@ app = dash.Dash(assets_folder=SETTINGS.ASSETS_PATH) -# Load pre-computed heatmap data -DATA_PATH = Path(__file__).parent / "reverse_pourbaix_heatmap.parquet" +# Load pre-computed heatmap data from the versioned, hive-partitioned S3 +# dataset; filter to one version so multiple versions in the bucket don't +# get mixed together. +DATA_PATH = "s3://materialsproject-build/collections/reverse-pourbaix-heatmap/" +DATA_FILTERS = [("version", "=", "2026-04-13")] -heatmap_data = ReversePourbaixDiagramComponent.load_heatmap_data(DATA_PATH) +heatmap_data = ReversePourbaixDiagramComponent.load_heatmap_data( + DATA_PATH, filters=DATA_FILTERS +) # Build the heatmap figure directly — no component, no callbacks. figure = ReversePourbaixDiagramComponent.get_heatmap_figure(heatmap_data) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet b/crystal_toolkit/apps/examples/reverse_pourbaix_heatmap.parquet deleted file mode 100644 index 876fe89a2fbe56d87e3cfe02fc59078959ba9a44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6559 zcmcgx30PBS);<9Yi=u2nu!tBn;sOEz6kH%|A?#~H!kP^TCV&u@U=1J8EmO3tDUIN*C)wb=s+RwAeaRt*xE+CMdKz^Z)A=JeTc>^N)=+s(!5(bA(cTH#EdPm$W&LzCBSj`uzrmW7OD zESZ_iTxKB~C1cAhWmYn4nT>3;Y>doSHWtX+S)7bGj)M_+G?6undaW%j?!67%gTOUl zYh}-}7znYpVJ@fM3o+p!2I8BeI6GKR0ZE~cLG})V94R$|ykhp*Bb2p7cJ}sMt{&6q48_x8%VrkK;!udSmDL9!{821AZ)-R8^C9>HA%Nd$!O3_E`hgG^t1X+E*P)PM z^dqH4^vz~jL+H(Go1!A7?6N=GR2bpUonYVga2(<9;5Z_$hkqsu76a|x49=>Vah#(j zbU!=YU22nxO#PEX^!x-O-6Wz1Q;F`3C30{gy6;NV$R&Eqi)dgp(IS+U-i>I_Y@*M? zh@7p7q>eaJ26D7oAt&}LW8-9RyyfcyeGeo}t?;Y&1N083} z??T8<0e`M9(R;uv1h#VMoX7REF+@Q&M61Tb?i|FELNsv&(N_4lJO#GIM7!F68#so8BRJ5MxCj4Fpz8(!Q_JGS1;K-IJ7JGs!!1Zp#coO_Vwl9b%5_t%G8QA)XsvsNf zPZXR_R0nL8ZxSsVL$uQqxkyINfms3{MZmlrn12E00_5N|toH!(pCHqK(z|2kX_LL}6L*iTd$EObNiBS&KRJ5|J2wk3j#sWkhBBkdF(%2p@llAi9k_oZL_3 z{W(#u9Z?))3+sqZyg`(5iO3SN2E@ifyx|+62h5XTFKh;pTN2U3QlgFRs6W`d0(|Li zM9WccW;2P#VDEk4JBaIIAIO2x3VT~7Ab#XCT!?ig>|k%DAJJXp@<1D6_zQS|>|+s8 zb1TuhLquIy5ijzyEf#oMh;|%C4g8fT*^%g*Xrfz9s7=(*S67JIvG?+P>?y(?@cb?8 z{SCbM15-0<5_o)hqW0?b{=d-E*#2IecS$Uka(K>*SGD553Ut8gcJflrha zk9=Ui9)AA;AJSsvY8%m?-$yNQh+al*@9aQasN34FQ4_F#D2eFpPNJp9iP(RK4d~?~ zj!}r?S76?AnWzx`W311z_I`80g`fd8xYL><7agw1WJ-81Mnp%;-O zWHS&$4s!BF5b(tz_o$aF)Xvqp$U87?se~MUXCl834Cud?2+fJPgIFhKV*e)eF=BY< z4#bc}ONi38g0s`;C&&_z+v~8K4$J|-9CwL`3wvWQd+vjeyB(;@14OeN5GQzaM|_cL zqOqv2Zs5BD%n9Hy2>to@IYbfA)8Tps@VkT%d5VcAn|n|Bb|oI(EYp+6ui%Ld*T;0tki-$m`iXDYZm2+R*r0|zgH2h@-O z{_MeL$Qtmq4>CveEiiw8I@*sKEUzJY6POF&d*LLabLgiHd?FF_zV^Tz@`b%fV218; z=*>q=4+Ox!1$7PX4xq;7x5D4ssMFiH86#Jhfp`2)qO+f(2OeM^Lly{`W)J-JV-^8# zAn=BPeq?B`~)!qZ!P%v2m0ht18U+$^b%xx;5`KlcMRP&kAL2f6pQzLwJOkh9 z=aH)~h}6I=hio!rQgA%$0%qFx;2b{1u~q6K>&T@8RhP?;w;nyB@S(gsm<=E4KX9LI{0!!jT%(GO~ZUOuN z^EJeLbQ<)aKRz5BLGLK?;Szz_E{C1vn5)jHsRif{a9#(DoPNvzU^xveXL^9=Q`FS2 zs9DGwAzKL?doB=7yNlU10rvsqqjfXp0kGwNjrl$v+`^Zz0{Pem-8YdFN7Nnq|8Np= zqeGpvpl^VM3v640P%G%26eao%nCqWGO?%@01|Itmy9MxlH5=HYkR#~itj0Z>gL@!& zPXp%#dy%WpQA@7KbsBQ81-{N>4#3WQ31$TN^gfMxA0&DOI(^U?>_j~QSKnn|fvt-R zz@rxP4|4shn2o?2It%p@gZqIRIbVtVKz0W;do%!fj79$T&AxlJWeB_In3@m|C zfXvuen`>$1w}1w7g@ZTvR>B*lzlPW0oGPJMfyKuYY)%HSC8zdKbNjS*f#u}x_OQ{u7uv(e37k429Fx;JBFD?SJEERha-l;wX{A$V z^puXYPSNz8-JLPBUcJy6JLi~FR~-LrT37tsFT1-E=HI!{l^DX}bSH(|r*|g{C$H~L ziS_O8PE8bW)~BT=r>{?!$k(qIXD;bqFUeiW*^r^^NZ%kW*tvd#Z1Jo88!}6daW-a^ zdqqgHD=*hea%yh9FUej0%czXJRkl$X@`i~G8HyIKz6|Bsg>0#+Gd@bH-jLlO&EKNy zlNRh)#+DWCYKxL-_H1vE743bcPquj9TkK5jYac~r>JDCR$Sgj5t1q+U=r8Q7(syiy zSxfpRHfEKb^g5nZ{_#S~Z2h@-VRpqQ*^Swi7j?(8tFA1w%&ETCCd@H>y}dD~=H@HM zbC%wI%QE-b?>`deF7x{5<;L9QKixW>Tl?@A%e)n4W25s{vfZ2VR@r!;$g3N>$V$H2 zAt72`@0`;lZ3AE>$#7FnyiQxZh#_2Qgn^@bG9p=lelYwl0mByS1g zZB}nF@V0379^!4)9lp=ow&Y~c^z9WF4byiRZXTNc+_InUPw!c26YROOenO4suI8DC zJ$JVS|K$06M@;Zb@@{d_jTbiM@4c~STjfpMVwe}l!Zj;^T-fExLbXoLu_BJmG&2{o z(rD8OOKkdND=YLR`T4{#!|EtyNky@qocnBC-CW~%9TRLwt=Y4O^``PTVpON4A zhrdaWlwz{{FZ~RKZF07P?R?OTH^}C75C1YU@(cQ~vysyO*_qi8c_vy-z7EcFJ~@wf z+mCk=1dbSrPmIX_5Qy`N7^H${<-A6KcwN|~@?w>|+~os{rCd$1N?o0+EYVizip#md z9+}y*J-DXJT;rPhcT(f!Qf{$ar-nMWG@6SI`5LuWmHS(n$w{u>P-;}n*Otijf&Pqk zafPnb@U#V;S}#}0^>P?4uFz_i&Gr~}B!1G5QS@JSW2){aeGSY1+YXrydWyeC;@`Q` z6zlnZPm=gp{Cf(SbjtPmGL51_Z%pMqzj$ruTfG9S3c z&c)CVn=8_3D`Y~ke}-Q|r6O9aR}{x<5;b9pjMSKI)7x)0$3JuJDWUNhnIz#Dxnqiej-|8LiD%MQf{u?CKQ4ph8uI)W>K; z=c`o-qZUguQVJQ{&@Wd+16%ZOut-e0vQn9(dU1wdK%qjCju-_RvBrq8N-7C}jfDT; zC%{*c5f+faPYF;8O?zd!>Pl%wvR{HJPQ45mWWp#PDHFdC`oij!f2x?z9K%{zeZH- z!*qp^c~c6b&=XNLI7NR61ZB)yH1lTc9MgMr8k%+-Mhor~Q5cDvON7pzv{F3 str: return f"{cutoff:.1f}" @staticmethod - def load_heatmap_data(path: str | Path) -> dict[str, Any]: + def load_heatmap_data( + path: str | Path, filters: list[tuple] | None = None + ) -> dict[str, Any]: """Load heatmap counts from a tidy, long-format parquet (one row per pH/V/cutoff combination, columns "pH", "V", "cutoff", "count") and reshape into the dict `get_heatmap_figure` expects: {"ph_values", "v_values", "cutoffs", "grid"}, where `grid` is a list of {"pH", "V", "counts": {cutoff_str: count}}. + + :param path: path (local or s3://) to the parquet data. + :param filters: optional pyarrow row-filter, e.g. + [("version", "=", "2026-04-13")]; see `__init__` for details. """ - df = pd.read_parquet(path) + df = pq.read_table(path, filters=filters).to_pandas() ph_values = sorted(df["pH"].unique().tolist()) v_values = sorted(df["V"].unique().tolist(), reverse=True) From 9dd91988d6231d883500cad1cea07f128b4798e5 Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 22 Jun 2026 13:48:54 -0700 Subject: [PATCH 28/33] moved path and some preprocessing out of component so component accepts raw data --- .../apps/examples/reverse_pourbaix_example.py | 6 +-- .../components/reverse_pourbaix.py | 54 +++++++------------ 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py index f6dd2bde..0f3902d8 100755 --- a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py +++ b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py @@ -7,6 +7,7 @@ from __future__ import annotations import dash +import pyarrow.parquet as pq from dash import dcc, html import crystal_toolkit.components as ctc @@ -21,9 +22,8 @@ DATA_PATH = "s3://materialsproject-build/collections/reverse-pourbaix-heatmap/" DATA_FILTERS = [("version", "=", "2026-04-13")] -heatmap_data = ReversePourbaixDiagramComponent.load_heatmap_data( - DATA_PATH, filters=DATA_FILTERS -) +heatmap_df = pq.read_table(DATA_PATH, filters=DATA_FILTERS).to_pandas() +heatmap_data = ReversePourbaixDiagramComponent.load_heatmap_data(heatmap_df) # Build the heatmap figure directly — no component, no callbacks. figure = ReversePourbaixDiagramComponent.get_heatmap_figure(heatmap_data) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 5bd0eade..dd794af3 100755 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -12,12 +12,10 @@ from __future__ import annotations import logging -from pathlib import Path from typing import Any import pandas as pd import plotly.graph_objects as go -import pyarrow.parquet as pq from dash import dcc, html from dash.dependencies import Component, Input, Output from dash.exceptions import PreventUpdate @@ -106,34 +104,27 @@ class ReversePourbaixDiagramComponent(MPComponent): def __init__( self, - parquet_path: str | Path | None = None, + stability_data: pd.DataFrame | None = None, *args, - filters: list[tuple] | None = None, **kwargs, ): """ Args: - parquet_path: path (local or s3://) to the precomputed - (pH, V, mp_id, decomposition_energy) parquet data. Loaded once at - component construction. If None, the click-to-list functionality - is disabled but the heatmap still works. Current parquet data is - computed with solid filter and default ion concentrations. - filters: optional pyarrow row-filter, e.g. - [("version", "=", "2026-04-13")]. Applied via - pyarrow.parquet.read_table's `filters` kwarg, so it also works to - select a single version out of a hive-partitioned dataset - (local or S3). + stability_data: precomputed (pH, V, mp_id, decomposition_energy) + data, e.g. read from a parquet dataset by the caller. Indexed + by (pH, V) once at component construction for fast cell-click + lookups. If None, the click-to-list functionality is disabled + but the heatmap still works. Data is expected to be computed + with solid filter and default ion concentrations. """ super().__init__(*args, **kwargs) self._stability_df: pd.DataFrame | None = None - if parquet_path is not None: - logger.info("Loading reverse-Pourbaix stability data from %s", parquet_path) - df = pq.read_table(parquet_path, filters=filters).to_pandas() + if stability_data is not None: # Index by (pH, V) for fast cell-click lookups. - self._stability_df = df.set_index(["pH", "V"]).sort_index() + self._stability_df = stability_data.set_index(["pH", "V"]).sort_index() logger.info( - "Loaded %d stability rows across %d cells", - len(df), + "Indexed %d stability rows across %d cells", + len(stability_data), self._stability_df.index.nunique(), ) @@ -161,21 +152,16 @@ def _format_cutoff_key(cutoff: float) -> str: return f"{cutoff:.1f}" @staticmethod - def load_heatmap_data( - path: str | Path, filters: list[tuple] | None = None - ) -> dict[str, Any]: - """Load heatmap counts from a tidy, long-format parquet (one row per - pH/V/cutoff combination, columns "pH", "V", "cutoff", "count") and - reshape into the dict `get_heatmap_figure` expects: - {"ph_values", "v_values", "cutoffs", "grid"}, where `grid` is a list - of {"pH", "V", "counts": {cutoff_str: count}}. - - :param path: path (local or s3://) to the parquet data. - :param filters: optional pyarrow row-filter, e.g. - [("version", "=", "2026-04-13")]; see `__init__` for details. + def load_heatmap_data(df: pd.DataFrame) -> dict[str, Any]: + """Reshape tidy, long-format heatmap counts (one row per pH/V/cutoff + combination, columns "pH", "V", "cutoff", "count") into the dict + `get_heatmap_figure` expects: {"ph_values", "v_values", "cutoffs", + "grid"}, where `grid` is a list of {"pH", "V", "counts": {cutoff_str: + count}}. + + :param df: tidy, long-format heatmap-count data, e.g. read from a + parquet dataset by the caller. """ - df = pq.read_table(path, filters=filters).to_pandas() - ph_values = sorted(df["pH"].unique().tolist()) v_values = sorted(df["V"].unique().tolist(), reverse=True) cutoffs = sorted(df["cutoff"].unique().tolist()) From 665e36b5eb7607e6f9226c7fe5d9f80c2885bcd8 Mon Sep 17 00:00:00 2001 From: zccalka Date: Mon, 22 Jun 2026 15:19:37 -0700 Subject: [PATCH 29/33] ran pre-commit --- .../apps/examples/reverse_pourbaix_example.py | 0 crystal_toolkit/components/reverse_pourbaix.py | 14 ++++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) mode change 100755 => 100644 crystal_toolkit/apps/examples/reverse_pourbaix_example.py mode change 100755 => 100644 crystal_toolkit/components/reverse_pourbaix.py diff --git a/crystal_toolkit/apps/examples/reverse_pourbaix_example.py b/crystal_toolkit/apps/examples/reverse_pourbaix_example.py old mode 100755 new mode 100644 diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py old mode 100755 new mode 100644 index dd794af3..a7acb8b1 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -12,9 +12,8 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any -import pandas as pd import plotly.graph_objects as go from dash import dcc, html from dash.dependencies import Component, Input, Output @@ -24,6 +23,9 @@ from crystal_toolkit.core.mpcomponent import MPComponent +if TYPE_CHECKING: + import pandas as pd + logger = logging.getLogger(__name__) __author__ = "Leo Karlsson" @@ -57,7 +59,6 @@ class ReversePourbaixDiagramComponent(MPComponent): default_plot_style = frozendict( xaxis={ - "title": "pH", "anchor": "y", "mirror": "ticks", "showgrid": False, @@ -71,7 +72,6 @@ class ReversePourbaixDiagramComponent(MPComponent): "range": [MIN_PH, MAX_PH], }, yaxis={ - "title": "Potential (V vs. SHE)", "anchor": "x", "mirror": "ticks", "range": [MIN_V, MAX_V], @@ -141,7 +141,7 @@ def _resolve_cutoff(value: float | list[float] | None) -> float: @staticmethod def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: """Snap a clicked (pH, V) point to the precomputed grid keys.""" - return int(round(ph)), round(v * 2) / 2 + return round(ph), round(v * 2) / 2 @staticmethod def _format_cutoff_key(cutoff: float) -> str: @@ -387,10 +387,8 @@ def update_figure( if isinstance(show_water_lines, list): show_water_lines = show_water_lines[0] if show_water_lines else True - figure = self.get_heatmap_figure( + return self.get_heatmap_figure( heatmap_data, stability_cutoff=self._resolve_cutoff(stability_cutoff), show_water_lines=bool(show_water_lines), ) - - return figure From c6bee46aaf54e736a07ed97ea19eb233ba38a363 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 2 Jul 2026 11:43:12 +0200 Subject: [PATCH 30/33] removed dead code, get_stable_mp_ids was replaced by _build_active_rows in web --- crystal_toolkit/components/reverse_pourbaix.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index a7acb8b1..69fbcc87 100644 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -187,22 +187,6 @@ def load_heatmap_data(df: pd.DataFrame) -> dict[str, Any]: "grid": grid, } - def get_stable_mp_ids(self, ph: float, v: float, cutoff: float) -> list[str]: - """Return mp_ids stable at (pH, V) below the given decomposition-energy cutoff. - - Returns an empty list if the parquet data is not loaded. - """ - if self._stability_df is None: - return [] - ph_key, v_key = self._snap_to_grid(ph, v) - try: - cell = self._stability_df.loc[(ph_key, v_key)] - except KeyError: - logger.debug("No stability data for (pH=%s, V=%s)", ph_key, v_key) - return [] - stable = cell[cell["decomposition_energy"] <= cutoff] - return stable["mp_id"].tolist() - @staticmethod def get_heatmap_figure( heatmap_data: dict[str, Any], From ad0467519f3bde064a5b1c10f289ef4addca0e76 Mon Sep 17 00:00:00 2001 From: zccalka Date: Thu, 2 Jul 2026 14:32:00 +0200 Subject: [PATCH 31/33] removed list as type form resolve cutoff --- crystal_toolkit/components/reverse_pourbaix.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index 69fbcc87..dbc13e4c 100644 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -129,11 +129,8 @@ def __init__( ) @staticmethod - def _resolve_cutoff(value: float | list[float] | None) -> float: - """Unwrap a slider value (which may be a list) to a float, falling back - to the default cutoff.""" - if isinstance(value, list): - value = value[0] if value else DEFAULT_CUTOFF + def _resolve_cutoff(value: float | None) -> float: + """Unwrap a slider value to a float, falling back to the default cutoff.""" if value is None: return DEFAULT_CUTOFF return float(value) From f0832591061accf64a80cc83499adc88e9584120 Mon Sep 17 00:00:00 2001 From: Min-Hsueh Chiu Date: Tue, 7 Jul 2026 09:39:02 -0700 Subject: [PATCH 32/33] extract stability_cutoff from get_slider_input output list --- crystal_toolkit/components/reverse_pourbaix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index dbc13e4c..fb78bcbc 100644 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -358,7 +358,7 @@ def generate_callbacks(self, app, cache) -> None: Input(self.get_kwarg_id("stability_cutoff"), "value"), ) def update_figure( - heatmap_json, show_water_lines, stability_cutoff + heatmap_json, show_water_lines, stability_cutoff_list ): # some options ignored for now if not heatmap_json: raise PreventUpdate @@ -370,6 +370,6 @@ def update_figure( return self.get_heatmap_figure( heatmap_data, - stability_cutoff=self._resolve_cutoff(stability_cutoff), + stability_cutoff=self._resolve_cutoff(stability_cutoff_list[0]), show_water_lines=bool(show_water_lines), ) From c6c382926c4f80681382c7e904798bcc3553c4d5 Mon Sep 17 00:00:00 2001 From: Min-Hsueh Chiu Date: Tue, 7 Jul 2026 09:43:35 -0700 Subject: [PATCH 33/33] _resolve_cutoff handles the output of MPComponent's get_slider_input which is a list --- crystal_toolkit/components/reverse_pourbaix.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crystal_toolkit/components/reverse_pourbaix.py b/crystal_toolkit/components/reverse_pourbaix.py index fb78bcbc..826c6f40 100644 --- a/crystal_toolkit/components/reverse_pourbaix.py +++ b/crystal_toolkit/components/reverse_pourbaix.py @@ -129,11 +129,11 @@ def __init__( ) @staticmethod - def _resolve_cutoff(value: float | None) -> float: - """Unwrap a slider value to a float, falling back to the default cutoff.""" - if value is None: + def _resolve_cutoff(li: list | None) -> float: + """Unwrap MPComponent's get_slider_input output list to a float, falling back to the default cutoff.""" + if not li: return DEFAULT_CUTOFF - return float(value) + return float(li[0]) @staticmethod def _snap_to_grid(ph: float, v: float) -> tuple[int, float]: @@ -358,7 +358,7 @@ def generate_callbacks(self, app, cache) -> None: Input(self.get_kwarg_id("stability_cutoff"), "value"), ) def update_figure( - heatmap_json, show_water_lines, stability_cutoff_list + heatmap_json, show_water_lines, stability_cutoff ): # some options ignored for now if not heatmap_json: raise PreventUpdate @@ -370,6 +370,6 @@ def update_figure( return self.get_heatmap_figure( heatmap_data, - stability_cutoff=self._resolve_cutoff(stability_cutoff_list[0]), + stability_cutoff=self._resolve_cutoff(stability_cutoff), show_water_lines=bool(show_water_lines), )