File size: 8,022 Bytes
6ac9514
 
 
 
 
 
 
 
 
 
b49b2a8
 
 
6ac9514
9dede68
6ac9514
 
b49b2a8
6ac9514
b49b2a8
 
6ac9514
 
 
 
 
 
 
 
 
 
 
 
 
9dede68
6ac9514
9dede68
 
 
 
873d39c
9dede68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
873d39c
 
9dede68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b49b2a8
7691a82
 
9a83e3d
 
 
 
 
 
 
 
7691a82
9a83e3d
 
 
7691a82
 
 
9a83e3d
9dede68
 
 
 
 
 
873d39c
9dede68
 
 
6ac9514
 
b49b2a8
 
6ac9514
9dede68
 
b49b2a8
7691a82
 
b670bbb
7691a82
b670bbb
 
 
 
7691a82
 
b670bbb
 
 
 
 
 
 
7691a82
ab3f0b9
7691a82
6ac9514
 
873d39c
ab3f0b9
 
 
 
 
 
7691a82
ab3f0b9
9dede68
 
 
 
 
6ac9514
9a83e3d
6ac9514
 
 
 
 
 
 
 
9dede68
 
 
 
 
 
 
 
 
 
 
6ac9514
 
b49b2a8
6ac9514
b49b2a8
 
 
6ac9514
873d39c
6ac9514
 
 
9dede68
 
 
6ac9514
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# Hugging Face ZeroGPU: import spaces at the top of the file
try:
    import spaces
    GPU = spaces.GPU
except Exception:
    def GPU(*args, **kwargs):
        def decorator(fn):
            return fn
        return decorator

import os
import sys
from pathlib import Path
import json
import traceback
import pandas as pd
import io

# Configure paths
ROOT_DIR = Path(__file__).resolve().parent
BACKEND_DIR = ROOT_DIR / "backend"
for d in [str(ROOT_DIR), str(BACKEND_DIR)]:
    if d not in sys.path:
        sys.path.insert(0, d)

# Import solver and backend handlers
try:
    from backend.cv_solver import solve_cv
except ImportError:
    try:
        from cv_solver import solve_cv
    except ImportError:
        from FD_solver import solve_cv

# Top-level @spaces.GPU function registered with Gradio for ZeroGPU A100 allocation
@GPU(duration=120)
def gradio_solve_cv(file_content: str, config_json: str):
    """ZeroGPU registered execution point for JAX optimization."""
    try:
        if not file_content or not file_content.strip():
            return json.dumps({"type": "error", "message": "No CSV data file content provided. Please upload a cyclic voltammetry data file."})

        raw_config = json.loads(config_json) if isinstance(config_json, str) else (config_json or {})
        config = {
            "scan_rate_v_s": float(raw_config.get("scan_rate", 0.010)),
            "film_thickness": float(raw_config.get("film_thickness", 1e-4)),
            "v_min": float(raw_config.get("v_min", -1.0)),
            "v_max": float(raw_config.get("v_max", 1.0)),
            "skip_factor": int(raw_config.get("skip_factor", 5)),
            "num_peaks": int(raw_config.get("num_peaks", 50)),
            "max_iter": int(raw_config.get("max_iter", 100)),
            "tol_ftol": float(raw_config.get("tol_ftol", 1e-8)),
            "tol_gtol": float(raw_config.get("tol_gtol", 1e-7)),
            "num_terms": int(raw_config.get("num_terms", 50)),
            "loss_weight_const": float(raw_config.get("loss_weight_const", 1.0))
        }
        pot_col = int(raw_config.get("pot_col", 0))
        cur_col = int(raw_config.get("cur_col", 1))

        df = pd.read_csv(io.StringIO(file_content), sep=None, engine='python')
        result_dict = solve_cv(df, config, pot_col, cur_col, queue=None, loop=None)

        return json.dumps({
            "type": "done",
            "params": {
                "D0": result_dict["parameters"]["diffusivity"],
                "Vc": result_dict["parameters"]["v_center"],
                "beta_L": result_dict["parameters"]["beta_left"],
                "beta_R": result_dict["parameters"]["beta_right"],
                "I_offset": result_dict["parameters"]["baseline_offset"]
            },
            "plots": {
                "v_plot": result_dict["plots"]["v_plot"],
                "d_of_v": result_dict["plots"]["d_of_v"],
                "dos_total": result_dict["plots"]["dos_total"],
                "dos_peaks": result_dict["plots"]["dos_matrix"],
                "exp_potential": result_dict["plots"]["exp_potential"],
                "exp_current": result_dict["plots"]["exp_current"],
                "sim_current": result_dict["plots"]["sim_current"]
            },
            "total_iterations": 100
        })
    except Exception as e:
        return json.dumps({
            "type": "error",
            "message": str(e),
            "trace": traceback.format_exc()
        })

def get_app_assets():
    """Load index.html, style.css, and app.js."""
    index_file = ROOT_DIR / "index.html"
    css_file = ROOT_DIR / "style.css"
    js_file = ROOT_DIR / "app.js"

    html_content = index_file.read_text(encoding="utf-8") if index_file.exists() else "<h2>Dashboard Loading...</h2>"
    css_content = css_file.read_text(encoding="utf-8") if css_file.exists() else ""
    js_content = js_file.read_text(encoding="utf-8") if js_file.exists() else ""

    # Inline CSS & JS for 100% self-contained execution
    inlined = html_content
    inlined = inlined.replace('<link rel="stylesheet" href="style.css">', f"<style>\n{css_content}\n</style>")
    inlined = inlined.replace('<script src="app.js"></script>', f"<script>\n{js_content}\n</script>")
    return inlined, css_content, js_content

inlined_html, app_css, app_js = get_app_assets()

# Health check helper
def health_info():
    import jax
    devices = [str(d) for d in jax.devices()]
    return {
        "status": "ok",
        "engine": "JAX Auto-Diff Hardware Accelerated Engine",
        "devices": devices
    }

# Gradio integration for Hugging Face ZeroGPU
has_gradio = False
try:
    import gradio as gr
    from fastapi.middleware.cors import CORSMiddleware
    from fastapi.requests import Request
    from fastapi.responses import JSONResponse

    head_html = f"""
    <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
    <style>
    :root, html, body {{
        color-scheme: dark !important;
        background-color: #0b1120 !important;
    }}
    {app_css}
    footer {{visibility: hidden !important; display: none !important;}}
    .gradio-container {{
        max-width: 100% !important; 
        padding: 0 !important; 
        margin: 0 !important; 
        background: #0b1120 !important;
        color: #e2e8f0 !important;
    }}
    .prose {{max-width: 100% !important;}}
    .hidden-bridge {{position: absolute !important; opacity: 0 !important; pointer-events: none !important; height: 0 !important; width: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; border: none !important;}}
    </style>
    """

    with gr.Blocks(title="Cyclic Voltammetry Parameter Extraction & Physical Model Fitting", head=head_html, js=app_js) as demo:
        # Native Gradio components for ZeroGPU hardware event dispatching
        gr_input_file = gr.Textbox(value="", elem_id="gr_input_file", elem_classes=["hidden-bridge"])
        gr_input_config = gr.Textbox(value="{}", elem_id="gr_input_config", elem_classes=["hidden-bridge"])
        gr_output_json = gr.Textbox(value="", elem_id="gr_output_json", elem_classes=["hidden-bridge"])
        gr_trigger_btn = gr.Button("Execute ZeroGPU", elem_id="gr_trigger_btn", elem_classes=["hidden-bridge"])

        gr.HTML(inlined_html)

        gr_trigger_btn.click(
            fn=gradio_solve_cv,
            inputs=[gr_input_file, gr_input_config],
            outputs=[gr_output_json]
        )

    # Attach FastAPI routes directly to demo.app
    demo.app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    async def api_solve_handler(request: Request):
        data = await request.json()
        file_content = data.get("file_content", "")
        config = data.get("config", {})
        res_str = gradio_solve_cv(file_content, json.dumps(config))
        return JSONResponse(content=json.loads(res_str))

    demo.app.add_api_route("/health", health_info, methods=["GET"])
    demo.app.add_api_route("/api/health", health_info, methods=["GET"])
    demo.app.add_api_route("/api/solve", api_solve_handler, methods=["POST"])
    demo.app.add_api_route("/solve", api_solve_handler, methods=["POST"])

    has_gradio = True
except ImportError:
    has_gradio = False

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 7860))
    if has_gradio:
        print(f"Starting Gradio server on port {port}...")
        demo.queue().launch(server_name="0.0.0.0", server_port=port, share=False)
    else:
        import uvicorn
        from fastapi import FastAPI
        fastapi_app = FastAPI()
        fastapi_app.add_api_route("/health", health_info, methods=["GET"])
        print(f"Starting FastAPI server on port {port}...")
        uvicorn.run(fastapi_app, host="0.0.0.0", port=port)