bingyan user Cursor commited on
Commit ·
8619a66
1
Parent(s): 3f4e2ae
Rebrand TRACE -> SPARK
Browse files- Full name: 'Simulation-based Posterior Amortization for Reaction Kinetics'
- Class TRACEPredictor -> SPARKPredictor; env vars TRACE_* -> SPARK_*
- Updated README, app.py title/info, all docstrings and comments.
- No model architecture or checkpoint format changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
- README.md +3 -3
- app.py +18 -18
- image_encoder.py +1 -1
- image_preprocessing.py +2 -2
- inference.py +14 -14
- plotting.py +1 -1
- preprocessing.py +3 -3
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: ⚡
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
@@ -8,10 +8,10 @@ sdk_version: "5.29.0"
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description:
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
Amortized Bayesian inference for electrochemistry and catalysis.
|
| 17 |
Upload cyclic voltammetry (CV) or temperature-programmed desorption (TPD) data to automatically:
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SPARK
|
| 3 |
emoji: ⚡
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
short_description: SPARK — Bayesian inference for CV & TPD analysis
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# SPARK — Simulation-based Posterior Amortization for Reaction Kinetics
|
| 15 |
|
| 16 |
Amortized Bayesian inference for electrochemistry and catalysis.
|
| 17 |
Upload cyclic voltammetry (CV) or temperature-programmed desorption (TPD) data to automatically:
|
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
Gradio web interface for mechanism classification and parameter inference
|
| 5 |
from cyclic voltammetry (CV) and temperature-programmed desorption (TPD) data.
|
|
@@ -16,7 +16,7 @@ import gradio as gr
|
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
-
from inference import
|
| 20 |
from preprocessing import (
|
| 21 |
nondimensionalize_cv,
|
| 22 |
estimate_E0,
|
|
@@ -41,27 +41,27 @@ DEMO_RENDERS = REPO_ROOT / "demo_renders"
|
|
| 41 |
EC_CHECKPOINT = REPO_ROOT / "checkpoints" / "ec_best.pt"
|
| 42 |
TPD_CHECKPOINT = REPO_ROOT / "checkpoints" / "tpd_best.pt"
|
| 43 |
|
| 44 |
-
# Allow override via environment variables.
|
| 45 |
# canonical name; ECFLOW_*_CHECKPOINT remains accepted for backward
|
| 46 |
# compatibility with existing HF Space secrets.
|
| 47 |
EC_CHECKPOINT = Path(
|
| 48 |
os.environ.get(
|
| 49 |
-
"
|
| 50 |
os.environ.get("ECFLOW_EC_CHECKPOINT", str(EC_CHECKPOINT)),
|
| 51 |
)
|
| 52 |
)
|
| 53 |
TPD_CHECKPOINT = Path(
|
| 54 |
os.environ.get(
|
| 55 |
-
"
|
| 56 |
os.environ.get("ECFLOW_TPD_CHECKPOINT", str(TPD_CHECKPOINT)),
|
| 57 |
)
|
| 58 |
)
|
| 59 |
|
| 60 |
# Optional image-input checkpoints. When set and the file exists, the
|
| 61 |
-
# "From Image" tabs use the image-input
|
| 62 |
# digitizing the plot. If not set, the digitizer fallback is used.
|
| 63 |
-
EC_IMAGE_CHECKPOINT_PATH = os.environ.get("
|
| 64 |
-
TPD_IMAGE_CHECKPOINT_PATH = os.environ.get("
|
| 65 |
EC_IMAGE_CHECKPOINT = (
|
| 66 |
Path(EC_IMAGE_CHECKPOINT_PATH) if EC_IMAGE_CHECKPOINT_PATH else None
|
| 67 |
)
|
|
@@ -73,8 +73,8 @@ TPD_IMAGE_CHECKPOINT = (
|
|
| 73 |
# "From Image" tabs expose a "Joint image+waveform (Phase 2)" inference
|
| 74 |
# strategy that fuses the rendered image with the digitizer-extracted
|
| 75 |
# waveform inside a single encoder. Falls back to ensemble if missing.
|
| 76 |
-
EC_JOINT_CHECKPOINT_PATH = os.environ.get("
|
| 77 |
-
TPD_JOINT_CHECKPOINT_PATH = os.environ.get("
|
| 78 |
EC_JOINT_CHECKPOINT = (
|
| 79 |
Path(EC_JOINT_CHECKPOINT_PATH) if EC_JOINT_CHECKPOINT_PATH else None
|
| 80 |
)
|
|
@@ -245,7 +245,7 @@ def get_predictor():
|
|
| 245 |
tpd_joint = (str(TPD_JOINT_CHECKPOINT)
|
| 246 |
if (TPD_JOINT_CHECKPOINT and TPD_JOINT_CHECKPOINT.exists())
|
| 247 |
else None)
|
| 248 |
-
predictor =
|
| 249 |
ec_checkpoint=ec_ckpt,
|
| 250 |
tpd_checkpoint=tpd_ckpt,
|
| 251 |
ec_image_checkpoint=ec_img,
|
|
@@ -359,7 +359,7 @@ def analyze_cv_image(files, scan_rate_text, E0_V, threshold, current_unit,
|
|
| 359 |
method_label="Ensemble (recommended)"):
|
| 360 |
"""Analyze CV from uploaded plot images (one per scan rate).
|
| 361 |
|
| 362 |
-
When the image-input
|
| 363 |
image-mode prediction in parallel with the digitizer + waveform-mode path,
|
| 364 |
and combine via `method_label` (Ensemble / Image-direct / Digitize-then-infer
|
| 365 |
/ Auto-fallback). When only the waveform model is available, falls back
|
|
@@ -477,7 +477,7 @@ def analyze_cv_image(files, scan_rate_text, E0_V, threshold, current_unit,
|
|
| 477 |
)
|
| 478 |
|
| 479 |
|
| 480 |
-
OOD_THRESHOLD = float(os.environ.get("
|
| 481 |
|
| 482 |
|
| 483 |
def _ood_banner_update(ood_score, threshold=OOD_THRESHOLD):
|
|
@@ -636,7 +636,7 @@ def _method_comparison_html(hybrid_out, threshold=OOD_THRESHOLD):
|
|
| 636 |
"methods disagree \u2014 review carefully</span>")
|
| 637 |
|
| 638 |
footnote = (
|
| 639 |
-
"Image-direct (Phase 1) is the image-only
|
| 640 |
"uses the headline waveform model on a curve extracted from the plot; "
|
| 641 |
"Joint (Phase 2) fuses both signals inside a single encoder trained "
|
| 642 |
"with real-world distortion augmentation. When available, the joint "
|
|
@@ -1254,7 +1254,7 @@ footer { display: none !important; }
|
|
| 1254 |
|
| 1255 |
def build_app():
|
| 1256 |
with gr.Blocks(
|
| 1257 |
-
title="
|
| 1258 |
theme=gr.themes.Soft(
|
| 1259 |
primary_hue="blue",
|
| 1260 |
secondary_hue="slate",
|
|
@@ -1265,7 +1265,7 @@ def build_app():
|
|
| 1265 |
gr.HTML("<div class='trace-page'>")
|
| 1266 |
gr.HTML(
|
| 1267 |
"<div class='main-header'>"
|
| 1268 |
-
"<h1><span class='accent'>
|
| 1269 |
"<p><strong>T</strong>race <strong>R</strong>ecognition and "
|
| 1270 |
"<strong>A</strong>mortized <strong>C</strong>onditional "
|
| 1271 |
"<strong>E</strong>stimation — Bayesian inference for "
|
|
@@ -1757,7 +1757,7 @@ def build_app():
|
|
| 1757 |
gr.Markdown("""
|
| 1758 |
## How it works
|
| 1759 |
|
| 1760 |
-
**
|
| 1761 |
**conditional normalizing flows** with a **Set Transformer** encoder to
|
| 1762 |
perform amortized Bayesian inference. Given one or more experimental
|
| 1763 |
curves, it simultaneously classifies the reaction mechanism and produces
|
|
@@ -1778,7 +1778,7 @@ weighting.
|
|
| 1778 |
### Citation
|
| 1779 |
|
| 1780 |
```
|
| 1781 |
-
Yan, B. (2026).
|
| 1782 |
Mechanism Identification and Parameter Estimation in
|
| 1783 |
Electrochemistry and Catalysis via Conditional
|
| 1784 |
Normalizing Flows. [Preprint]
|
|
|
|
| 1 |
"""
|
| 2 |
+
SPARK (Simulation-based Posterior Amortization for Reaction Kinetics)
|
| 3 |
|
| 4 |
Gradio web interface for mechanism classification and parameter inference
|
| 5 |
from cyclic voltammetry (CV) and temperature-programmed desorption (TPD) data.
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
+
from inference import SPARKPredictor
|
| 20 |
from preprocessing import (
|
| 21 |
nondimensionalize_cv,
|
| 22 |
estimate_E0,
|
|
|
|
| 41 |
EC_CHECKPOINT = REPO_ROOT / "checkpoints" / "ec_best.pt"
|
| 42 |
TPD_CHECKPOINT = REPO_ROOT / "checkpoints" / "tpd_best.pt"
|
| 43 |
|
| 44 |
+
# Allow override via environment variables. SPARK_*_CHECKPOINT is the
|
| 45 |
# canonical name; ECFLOW_*_CHECKPOINT remains accepted for backward
|
| 46 |
# compatibility with existing HF Space secrets.
|
| 47 |
EC_CHECKPOINT = Path(
|
| 48 |
os.environ.get(
|
| 49 |
+
"SPARK_EC_CHECKPOINT",
|
| 50 |
os.environ.get("ECFLOW_EC_CHECKPOINT", str(EC_CHECKPOINT)),
|
| 51 |
)
|
| 52 |
)
|
| 53 |
TPD_CHECKPOINT = Path(
|
| 54 |
os.environ.get(
|
| 55 |
+
"SPARK_TPD_CHECKPOINT",
|
| 56 |
os.environ.get("ECFLOW_TPD_CHECKPOINT", str(TPD_CHECKPOINT)),
|
| 57 |
)
|
| 58 |
)
|
| 59 |
|
| 60 |
# Optional image-input checkpoints. When set and the file exists, the
|
| 61 |
+
# "From Image" tabs use the image-input SPARK model directly instead of
|
| 62 |
# digitizing the plot. If not set, the digitizer fallback is used.
|
| 63 |
+
EC_IMAGE_CHECKPOINT_PATH = os.environ.get("SPARK_EC_IMAGE_CHECKPOINT", "")
|
| 64 |
+
TPD_IMAGE_CHECKPOINT_PATH = os.environ.get("SPARK_TPD_IMAGE_CHECKPOINT", "")
|
| 65 |
EC_IMAGE_CHECKPOINT = (
|
| 66 |
Path(EC_IMAGE_CHECKPOINT_PATH) if EC_IMAGE_CHECKPOINT_PATH else None
|
| 67 |
)
|
|
|
|
| 73 |
# "From Image" tabs expose a "Joint image+waveform (Phase 2)" inference
|
| 74 |
# strategy that fuses the rendered image with the digitizer-extracted
|
| 75 |
# waveform inside a single encoder. Falls back to ensemble if missing.
|
| 76 |
+
EC_JOINT_CHECKPOINT_PATH = os.environ.get("SPARK_EC_JOINT_CHECKPOINT", "")
|
| 77 |
+
TPD_JOINT_CHECKPOINT_PATH = os.environ.get("SPARK_TPD_JOINT_CHECKPOINT", "")
|
| 78 |
EC_JOINT_CHECKPOINT = (
|
| 79 |
Path(EC_JOINT_CHECKPOINT_PATH) if EC_JOINT_CHECKPOINT_PATH else None
|
| 80 |
)
|
|
|
|
| 245 |
tpd_joint = (str(TPD_JOINT_CHECKPOINT)
|
| 246 |
if (TPD_JOINT_CHECKPOINT and TPD_JOINT_CHECKPOINT.exists())
|
| 247 |
else None)
|
| 248 |
+
predictor = SPARKPredictor(
|
| 249 |
ec_checkpoint=ec_ckpt,
|
| 250 |
tpd_checkpoint=tpd_ckpt,
|
| 251 |
ec_image_checkpoint=ec_img,
|
|
|
|
| 359 |
method_label="Ensemble (recommended)"):
|
| 360 |
"""Analyze CV from uploaded plot images (one per scan rate).
|
| 361 |
|
| 362 |
+
When the image-input SPARK model is available we run a hybrid pipeline:
|
| 363 |
image-mode prediction in parallel with the digitizer + waveform-mode path,
|
| 364 |
and combine via `method_label` (Ensemble / Image-direct / Digitize-then-infer
|
| 365 |
/ Auto-fallback). When only the waveform model is available, falls back
|
|
|
|
| 477 |
)
|
| 478 |
|
| 479 |
|
| 480 |
+
OOD_THRESHOLD = float(os.environ.get("SPARK_OOD_THRESHOLD", "0.5"))
|
| 481 |
|
| 482 |
|
| 483 |
def _ood_banner_update(ood_score, threshold=OOD_THRESHOLD):
|
|
|
|
| 636 |
"methods disagree \u2014 review carefully</span>")
|
| 637 |
|
| 638 |
footnote = (
|
| 639 |
+
"Image-direct (Phase 1) is the image-only SPARK; Digitize-then-infer "
|
| 640 |
"uses the headline waveform model on a curve extracted from the plot; "
|
| 641 |
"Joint (Phase 2) fuses both signals inside a single encoder trained "
|
| 642 |
"with real-world distortion augmentation. When available, the joint "
|
|
|
|
| 1254 |
|
| 1255 |
def build_app():
|
| 1256 |
with gr.Blocks(
|
| 1257 |
+
title="SPARK — Bayesian Inference for Electrochemistry & Catalysis",
|
| 1258 |
theme=gr.themes.Soft(
|
| 1259 |
primary_hue="blue",
|
| 1260 |
secondary_hue="slate",
|
|
|
|
| 1265 |
gr.HTML("<div class='trace-page'>")
|
| 1266 |
gr.HTML(
|
| 1267 |
"<div class='main-header'>"
|
| 1268 |
+
"<h1><span class='accent'>SPARK</span></h1>"
|
| 1269 |
"<p><strong>T</strong>race <strong>R</strong>ecognition and "
|
| 1270 |
"<strong>A</strong>mortized <strong>C</strong>onditional "
|
| 1271 |
"<strong>E</strong>stimation — Bayesian inference for "
|
|
|
|
| 1757 |
gr.Markdown("""
|
| 1758 |
## How it works
|
| 1759 |
|
| 1760 |
+
**SPARK** (Simulation-based Posterior Amortization for Reaction Kinetics) uses
|
| 1761 |
**conditional normalizing flows** with a **Set Transformer** encoder to
|
| 1762 |
perform amortized Bayesian inference. Given one or more experimental
|
| 1763 |
curves, it simultaneously classifies the reaction mechanism and produces
|
|
|
|
| 1778 |
### Citation
|
| 1779 |
|
| 1780 |
```
|
| 1781 |
+
Yan, B. (2026). SPARK: Amortized Bayesian Inference for
|
| 1782 |
Mechanism Identification and Parameter Estimation in
|
| 1783 |
Electrochemistry and Catalysis via Conditional
|
| 1784 |
Normalizing Flows. [Preprint]
|
image_encoder.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Image encoder for the image-input
|
| 3 |
|
| 4 |
Replaces the per-scan 1-D `SignalEncoder` with a small 2-D CNN that maps a
|
| 5 |
single rasterized plot image (grayscale, 224x224 by default) to a context
|
|
|
|
| 1 |
"""
|
| 2 |
+
Image encoder for the image-input SPARK variants (CV and TPD).
|
| 3 |
|
| 4 |
Replaces the per-scan 1-D `SignalEncoder` with a small 2-D CNN that maps a
|
| 5 |
single rasterized plot image (grayscale, 224x224 by default) to a context
|
image_preprocessing.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Image preprocessing for image-input
|
| 3 |
|
| 4 |
Real user uploads (paper-figure crops, software screenshots, photos of
|
| 5 |
lab monitors) live in a much wider image distribution than the rendered
|
|
@@ -250,7 +250,7 @@ def prepare_for_image_mode(
|
|
| 250 |
do_clean: bool = True,
|
| 251 |
target_size: int = 224,
|
| 252 |
) -> Tuple[Image.Image, Dict[str, object]]:
|
| 253 |
-
"""Full preprocessing pipeline for image-mode
|
| 254 |
|
| 255 |
Steps (any can be skipped):
|
| 256 |
crop_to_plot_region -> remove_gridlines_and_background -> resize.
|
|
|
|
| 1 |
"""
|
| 2 |
+
Image preprocessing for image-input SPARK on real-world uploads.
|
| 3 |
|
| 4 |
Real user uploads (paper-figure crops, software screenshots, photos of
|
| 5 |
lab monitors) live in a much wider image distribution than the rendered
|
|
|
|
| 250 |
do_clean: bool = True,
|
| 251 |
target_size: int = 224,
|
| 252 |
) -> Tuple[Image.Image, Dict[str, object]]:
|
| 253 |
+
"""Full preprocessing pipeline for image-mode SPARK.
|
| 254 |
|
| 255 |
Steps (any can be skipped):
|
| 256 |
crop_to_plot_region -> remove_gridlines_and_background -> resize.
|
inference.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
Loads trained EC and TPD models and runs end-to-end inference
|
| 5 |
from preprocessed arrays (dimensionless for CV, physical for TPD).
|
|
@@ -37,7 +37,7 @@ def _fix_actnorm_initialized(model):
|
|
| 37 |
module.initialized = True
|
| 38 |
|
| 39 |
|
| 40 |
-
class
|
| 41 |
"""Unified predictor for both EC (cyclic voltammetry) and TPD domains."""
|
| 42 |
|
| 43 |
def __init__(self, ec_checkpoint=None, tpd_checkpoint=None, device=None,
|
|
@@ -145,7 +145,7 @@ class TRACEPredictor:
|
|
| 145 |
k for k in unexpected if not k.endswith("_initialized")
|
| 146 |
]
|
| 147 |
if suspicious_missing or suspicious_unexpected:
|
| 148 |
-
print(f"[
|
| 149 |
if suspicious_missing:
|
| 150 |
print(f" missing ({len(suspicious_missing)}): "
|
| 151 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -231,7 +231,7 @@ class TRACEPredictor:
|
|
| 231 |
k for k in unexpected if not k.endswith("_initialized")
|
| 232 |
]
|
| 233 |
if suspicious_missing or suspicious_unexpected:
|
| 234 |
-
print(f"[
|
| 235 |
if suspicious_missing:
|
| 236 |
print(f" missing ({len(suspicious_missing)}): "
|
| 237 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -338,7 +338,7 @@ class TRACEPredictor:
|
|
| 338 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 339 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 340 |
if suspicious_missing or suspicious_unexpected:
|
| 341 |
-
print("[
|
| 342 |
if suspicious_missing:
|
| 343 |
print(f" missing ({len(suspicious_missing)}): "
|
| 344 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -393,7 +393,7 @@ class TRACEPredictor:
|
|
| 393 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 394 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 395 |
if suspicious_missing or suspicious_unexpected:
|
| 396 |
-
print("[
|
| 397 |
if suspicious_missing:
|
| 398 |
print(f" missing ({len(suspicious_missing)}): "
|
| 399 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -447,7 +447,7 @@ class TRACEPredictor:
|
|
| 447 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 448 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 449 |
if suspicious_missing or suspicious_unexpected:
|
| 450 |
-
print("[
|
| 451 |
if suspicious_missing:
|
| 452 |
print(f" missing ({len(suspicious_missing)}): "
|
| 453 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -503,7 +503,7 @@ class TRACEPredictor:
|
|
| 503 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 504 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 505 |
if suspicious_missing or suspicious_unexpected:
|
| 506 |
-
print("[
|
| 507 |
if suspicious_missing:
|
| 508 |
print(f" missing ({len(suspicious_missing)}): "
|
| 509 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
@@ -900,7 +900,7 @@ class TRACEPredictor:
|
|
| 900 |
n_samples=n_samples, temperature=temperature,
|
| 901 |
)
|
| 902 |
except Exception as exc:
|
| 903 |
-
print(f"[
|
| 904 |
|
| 905 |
if self.ec_model is not None and potentials is not None and fluxes is not None:
|
| 906 |
try:
|
|
@@ -909,7 +909,7 @@ class TRACEPredictor:
|
|
| 909 |
n_samples=n_samples, temperature=temperature,
|
| 910 |
)
|
| 911 |
except Exception as exc:
|
| 912 |
-
print(f"[
|
| 913 |
|
| 914 |
if (self.has_ec_joint_model and preprocessed is not None
|
| 915 |
and potentials is not None and fluxes is not None):
|
|
@@ -919,7 +919,7 @@ class TRACEPredictor:
|
|
| 919 |
n_samples=n_samples, temperature=temperature,
|
| 920 |
)
|
| 921 |
except Exception as exc:
|
| 922 |
-
print(f"[
|
| 923 |
|
| 924 |
return image_result, waveform_result, joint_result, preproc_meta
|
| 925 |
|
|
@@ -1014,7 +1014,7 @@ class TRACEPredictor:
|
|
| 1014 |
n_samples=n_samples, temperature=temperature,
|
| 1015 |
)
|
| 1016 |
except Exception as exc:
|
| 1017 |
-
print(f"[
|
| 1018 |
|
| 1019 |
if self.tpd_model is not None and temperatures is not None and rates is not None:
|
| 1020 |
try:
|
|
@@ -1023,7 +1023,7 @@ class TRACEPredictor:
|
|
| 1023 |
n_samples=n_samples, temperature=temperature,
|
| 1024 |
)
|
| 1025 |
except Exception as exc:
|
| 1026 |
-
print(f"[
|
| 1027 |
|
| 1028 |
if (self.has_tpd_joint_model and preprocessed is not None
|
| 1029 |
and temperatures is not None and rates is not None):
|
|
@@ -1033,7 +1033,7 @@ class TRACEPredictor:
|
|
| 1033 |
n_samples=n_samples, temperature=temperature,
|
| 1034 |
)
|
| 1035 |
except Exception as exc:
|
| 1036 |
-
print(f"[
|
| 1037 |
|
| 1038 |
return image_result, waveform_result, joint_result, preproc_meta
|
| 1039 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
SPARK inference engine.
|
| 3 |
|
| 4 |
Loads trained EC and TPD models and runs end-to-end inference
|
| 5 |
from preprocessed arrays (dimensionless for CV, physical for TPD).
|
|
|
|
| 37 |
module.initialized = True
|
| 38 |
|
| 39 |
|
| 40 |
+
class SPARKPredictor:
|
| 41 |
"""Unified predictor for both EC (cyclic voltammetry) and TPD domains."""
|
| 42 |
|
| 43 |
def __init__(self, ec_checkpoint=None, tpd_checkpoint=None, device=None,
|
|
|
|
| 145 |
k for k in unexpected if not k.endswith("_initialized")
|
| 146 |
]
|
| 147 |
if suspicious_missing or suspicious_unexpected:
|
| 148 |
+
print(f"[SPARKPredictor] WARNING: state_dict mismatch on EC ckpt.")
|
| 149 |
if suspicious_missing:
|
| 150 |
print(f" missing ({len(suspicious_missing)}): "
|
| 151 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 231 |
k for k in unexpected if not k.endswith("_initialized")
|
| 232 |
]
|
| 233 |
if suspicious_missing or suspicious_unexpected:
|
| 234 |
+
print(f"[SPARKPredictor] WARNING: state_dict mismatch on TPD ckpt.")
|
| 235 |
if suspicious_missing:
|
| 236 |
print(f" missing ({len(suspicious_missing)}): "
|
| 237 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 338 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 339 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 340 |
if suspicious_missing or suspicious_unexpected:
|
| 341 |
+
print("[SPARKPredictor] WARNING: state_dict mismatch on EC image ckpt.")
|
| 342 |
if suspicious_missing:
|
| 343 |
print(f" missing ({len(suspicious_missing)}): "
|
| 344 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 393 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 394 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 395 |
if suspicious_missing or suspicious_unexpected:
|
| 396 |
+
print("[SPARKPredictor] WARNING: state_dict mismatch on TPD image ckpt.")
|
| 397 |
if suspicious_missing:
|
| 398 |
print(f" missing ({len(suspicious_missing)}): "
|
| 399 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 447 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 448 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 449 |
if suspicious_missing or suspicious_unexpected:
|
| 450 |
+
print("[SPARKPredictor] WARNING: state_dict mismatch on EC joint ckpt.")
|
| 451 |
if suspicious_missing:
|
| 452 |
print(f" missing ({len(suspicious_missing)}): "
|
| 453 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 503 |
suspicious_missing = [k for k in missing if not k.endswith("_initialized")]
|
| 504 |
suspicious_unexpected = [k for k in unexpected if not k.endswith("_initialized")]
|
| 505 |
if suspicious_missing or suspicious_unexpected:
|
| 506 |
+
print("[SPARKPredictor] WARNING: state_dict mismatch on TPD joint ckpt.")
|
| 507 |
if suspicious_missing:
|
| 508 |
print(f" missing ({len(suspicious_missing)}): "
|
| 509 |
f"{suspicious_missing[:6]}{' ...' if len(suspicious_missing) > 6 else ''}")
|
|
|
|
| 900 |
n_samples=n_samples, temperature=temperature,
|
| 901 |
)
|
| 902 |
except Exception as exc:
|
| 903 |
+
print(f"[SPARKPredictor] image-mode CV failed: {exc}")
|
| 904 |
|
| 905 |
if self.ec_model is not None and potentials is not None and fluxes is not None:
|
| 906 |
try:
|
|
|
|
| 909 |
n_samples=n_samples, temperature=temperature,
|
| 910 |
)
|
| 911 |
except Exception as exc:
|
| 912 |
+
print(f"[SPARKPredictor] waveform CV failed: {exc}")
|
| 913 |
|
| 914 |
if (self.has_ec_joint_model and preprocessed is not None
|
| 915 |
and potentials is not None and fluxes is not None):
|
|
|
|
| 919 |
n_samples=n_samples, temperature=temperature,
|
| 920 |
)
|
| 921 |
except Exception as exc:
|
| 922 |
+
print(f"[SPARKPredictor] joint CV failed: {exc}")
|
| 923 |
|
| 924 |
return image_result, waveform_result, joint_result, preproc_meta
|
| 925 |
|
|
|
|
| 1014 |
n_samples=n_samples, temperature=temperature,
|
| 1015 |
)
|
| 1016 |
except Exception as exc:
|
| 1017 |
+
print(f"[SPARKPredictor] image-mode TPD failed: {exc}")
|
| 1018 |
|
| 1019 |
if self.tpd_model is not None and temperatures is not None and rates is not None:
|
| 1020 |
try:
|
|
|
|
| 1023 |
n_samples=n_samples, temperature=temperature,
|
| 1024 |
)
|
| 1025 |
except Exception as exc:
|
| 1026 |
+
print(f"[SPARKPredictor] waveform TPD failed: {exc}")
|
| 1027 |
|
| 1028 |
if (self.has_tpd_joint_model and preprocessed is not None
|
| 1029 |
and temperatures is not None and rates is not None):
|
|
|
|
| 1033 |
n_samples=n_samples, temperature=temperature,
|
| 1034 |
)
|
| 1035 |
except Exception as exc:
|
| 1036 |
+
print(f"[SPARKPredictor] joint TPD failed: {exc}")
|
| 1037 |
|
| 1038 |
return image_result, waveform_result, joint_result, preproc_meta
|
| 1039 |
|
plotting.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Visualization utilities for the
|
| 3 |
|
| 4 |
Generates matplotlib figures for mechanism classification, parameter
|
| 5 |
posteriors, and signal reconstruction overlays.
|
|
|
|
| 1 |
"""
|
| 2 |
+
Visualization utilities for the SPARK web app.
|
| 3 |
|
| 4 |
Generates matplotlib figures for mechanism classification, parameter
|
| 5 |
posteriors, and signal reconstruction overlays.
|
preprocessing.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Preprocessing utilities for
|
| 3 |
|
| 4 |
Handles:
|
| 5 |
- CSV/NPZ parsing for both CV and TPD data
|
|
@@ -24,7 +24,7 @@ def nondimensionalize_cv(E_volts, i_amps, v_Vs, E0_V, T_K=298.15,
|
|
| 24 |
A_cm2=0.0707, C_A_molcm3=1e-6, D_A_cm2s=1e-5, n=1,
|
| 25 |
v_ref_Vs=0.1):
|
| 26 |
"""
|
| 27 |
-
Convert physical CV data to dimensionless units for the
|
| 28 |
|
| 29 |
Potential and current are nondimensionalized using the Compton convention
|
| 30 |
with the scan-rate-dependent diffusion length d = sqrt(D·RT/(nFv)):
|
|
@@ -33,7 +33,7 @@ def nondimensionalize_cv(E_volts, i_amps, v_Vs, E0_V, T_K=298.15,
|
|
| 33 |
|
| 34 |
The dimensionless scan rate σ = v / v_ref is computed separately.
|
| 35 |
In the Compton convention σ ≡ 1 by construction (d absorbs v), but the
|
| 36 |
-
|
| 37 |
experiments at different scan rates. Setting v_ref so that σ spans the
|
| 38 |
training range (~0.1–100) gives the model the scan-rate information.
|
| 39 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
Preprocessing utilities for SPARK web app.
|
| 3 |
|
| 4 |
Handles:
|
| 5 |
- CSV/NPZ parsing for both CV and TPD data
|
|
|
|
| 24 |
A_cm2=0.0707, C_A_molcm3=1e-6, D_A_cm2s=1e-5, n=1,
|
| 25 |
v_ref_Vs=0.1):
|
| 26 |
"""
|
| 27 |
+
Convert physical CV data to dimensionless units for the SPARK model.
|
| 28 |
|
| 29 |
Potential and current are nondimensionalized using the Compton convention
|
| 30 |
with the scan-rate-dependent diffusion length d = sqrt(D·RT/(nFv)):
|
|
|
|
| 33 |
|
| 34 |
The dimensionless scan rate σ = v / v_ref is computed separately.
|
| 35 |
In the Compton convention σ ≡ 1 by construction (d absorbs v), but the
|
| 36 |
+
SPARK model uses σ as an explicit conditioning variable to distinguish
|
| 37 |
experiments at different scan rates. Setting v_ref so that σ spans the
|
| 38 |
training range (~0.1–100) gives the model the scan-rate information.
|
| 39 |
|