import time import gradio as gr # ----- 고정 기본값 ----- DEFAULTS = dict( Hn=8.0, Qn=3.5, rain_thr=40.0, dp_limit=3.0, soc_limit=90.0, rho=1000.0, g=9.80665 ) # ===== 계산 로직 ===== def electrical_power_kw(Q, H, eta, rho=DEFAULTS["rho"], g=DEFAULTS["g"]): if Q <= 0 or H <= 0 or eta <= 0: return 0.0 return (rho*g*Q*H*eta)/1000.0 def efficiency_from_relative_flow(r): r = max(0.0, min(1.3, float(r))) pts = [(0.00,0.00),(0.10,0.40),(0.30,0.60),(0.50,0.78), (0.70,0.86),(0.90,0.90),(1.00,0.91),(1.20,0.88),(1.30,0.85)] for (x1,y1),(x2,y2) in zip(pts[:-1], pts[1:]): if x1 <= r <= x2: t = 0 if x2==x1 else (r-x1)/(x2-x1) return y1 + t*(y2-y1) return 0.0 def decide_mode(Q, Qn, rain, dp, soc, rain_thr, dp_limit, soc_limit): if (dp is not None and dp > dp_limit) or (soc is not None and soc >= soc_limit): return "정비" if Q < 0.4*Qn: return "저유량" if (Q > 1.2*Qn) or (rain >= rain_thr): return "고유량" return "정상" def mode_policy(mode, Q, Qn, Hn): r = Q/Qn if Qn>0 else 0.0 eta = efficiency_from_relative_flow(r) if mode=="정상": eta_eff = max(eta, 0.88) p = electrical_power_kw(Q, Hn, eta_eff) return dict(eta=eta_eff, p_kw=p, curtail=False, bypass=False, service=False, note="효율 최적점 운전") if mode=="저유량": eta_eff = max(0.70, eta*0.95) p = electrical_power_kw(Q, Hn, eta_eff) * 0.90 return dict(eta=eta_eff, p_kw=p, curtail=False, bypass=False, service=False, note="출력 축소·효율 최적화") if mode=="고유량": eta_eff = max(0.68, eta*0.90) p = electrical_power_kw(Q, Hn, eta_eff) * 0.60 return dict(eta=eta_eff, p_kw=p, curtail=True, bypass=True, service=False, note="출력 제한·바이패스 병행") if mode=="정비": return dict(eta=0.0, p_kw=0.0, curtail=True, bypass=True, service=True, note="발전 중지·정비 필요") return dict(eta=0.0, p_kw=0.0, curtail=False, bypass=False, service=False, note="") def mode_badge(mode:str)->str: colors={"정상":"#22c55e","저유량":"#eab308","고유량":"#f97316","정비":"#ef4444"} col=colors.get(mode,"#6b7280") icon = "●" if mode!="정비" else "⚠️" return f"""
{icon}{'정비 모드' if mode=='정비' else '운전 모드 : '+mode}
""" def donut(label:str, value:float, maxv:float, unit:str=""): disp_val = round(float(value), 1) maxv = max(1e-6, float(maxv)) pct = max(0.0, min(float(value)/maxv, 1.0)) size = 220 r = 86 stroke = 18 C = 2*3.14159*r offset = C*(1-pct) return f"""
{disp_val:.1f}{unit} {label}
""" def chip(label:str, on:bool, on_color="#10b981"): bg=on_color if on else "#e5e7eb" fg="#fff" if on else "#111827" txt="ON" if on else "OFF" return f"""
{label}: {txt}
""" def action_box(mode:str)->str: if mode=="정비": body="""""" style='border:1px solid #fecaca;background:#fef2f2;' elif mode=="고유량": body='
출력 제한·바이패스 병행
' style='border:1px solid #fde68a;background:#fffbeb;' elif mode=="저유량": body='
출력 축소·효율 최적화
' style='border:1px solid #e5e7eb;background:#f9fafb;' else: body='
효율 최적점 운전
' style='border:1px solid #e5e7eb;background:#f9fafb;' return f"""
{body}
""" MODE_COLOR = {"정상":"#16a34a","저유량":"#eab308","고유량":"#f59e0b","정비":"#dc2626"} def _evaluate(Q_in, rain_mm, dp_kpa, soc_pct, Hn=DEFAULTS["Hn"], Qn=DEFAULTS["Qn"], rain_thr=DEFAULTS["rain_thr"], dp_limit=DEFAULTS["dp_limit"], soc_limit=DEFAULTS["soc_limit"]): mode = decide_mode(Q_in, Qn, rain_mm, dp_kpa, soc_pct, rain_thr, dp_limit, soc_limit) st = mode_policy(mode, Q_in, Qn, Hn) p_ref = electrical_power_kw(Qn, Hn, 0.9) header = mode_badge(mode) donuts = f"""
{donut("효율(η)", st['eta']*100, 100, "%")} {donut("출력(kW)", st['p_kw'], max(1.0, p_ref), " kW")}
""" chips = f"""
{chip("출력 제한", st['curtail'])} {chip("바이패스", st['bypass'])} {chip("정비 필요", st['service'], on_color="#ef4444")}
""" color = MODE_COLOR.get(mode, "#111827") summary = (f"
현재 모드: {mode}
\n\n" f"- 현재 유량 Q: {Q_in:.1f} m³/s (기준 Qₙ={Qn:.1f})\n" f"- 강수량: {rain_mm:.1f} mm/24h (허용 {rain_thr:.1f})\n" f"- 트래시랙: {dp_kpa:.1f} kPa (허용 {dp_limit:.1f})\n" f"- ESS 충전상태: {soc_pct:.1f}% (허용 {soc_limit:.1f}%)\n\n" f"▶ {st['note']}") panel = f"""
{header}{donuts}{chips}
""" extra = action_box(mode) return summary, panel, extra # ===== 외부 노출 함수 ===== def run(Q_in, rain_mm, dp_kpa, soc_pct): try: Q_in=float(Q_in); rain_mm=float(rain_mm); dp_kpa=float(dp_kpa); soc_pct=float(soc_pct) except Exception: return "⚠️ 입력 오류를 확인하세요.","","" return _evaluate(Q_in, rain_mm, dp_kpa, soc_pct) def preset_normal_and_run(): Q=0.8*DEFAULTS["Qn"]; rain=10.0; dp=1.0; soc=50.0 md, panel, extra = _evaluate(Q, rain, dp, soc) return round(Q,1), round(rain,1), round(dp,1), round(soc,1), md, panel, extra def preset_low_and_run(): Q=0.3*DEFAULTS["Qn"]; rain=5.0; dp=1.0; soc=50.0 md, panel, extra = _evaluate(Q, rain, dp, soc) return round(Q,1), round(rain,1), round(dp,1), round(soc,1), md, panel, extra def preset_high_and_run(): Q=1.3*DEFAULTS["Qn"]; rain=max(50.0, DEFAULTS["rain_thr"]); dp=1.0; soc=50.0 md, panel, extra = _evaluate(Q, rain, dp, soc) return round(Q,1), round(rain,1), round(dp,1), round(soc,1), md, panel, extra def preset_maint_and_run(): Q=0.8*DEFAULTS["Qn"]; rain=10.0; dp=DEFAULTS["dp_limit"]+0.5; soc=50.0 md, panel, extra = _evaluate(Q, rain, dp, soc) return round(Q,1), round(rain,1), round(dp,1), round(soc,1), md, panel, extra def autoplay(): steps = [preset_normal_and_run, preset_low_and_run, preset_high_and_run, preset_maint_and_run] for fn in steps: Q, rain, dp, soc, md, panel, extra = fn() yield (Q, rain, dp, soc, md, panel, extra) time.sleep(2.0) # ===== Gradio UI: 3열로 재배치 (가운데에 '즉시 실행') ===== with gr.Blocks(theme=gr.themes.Soft(), title="소수력 스마트 운전모드 대시보드") as app: gr.HTML(""" """) gr.Markdown("## 🌊 소수력 스마트 운전모드 대시보드") with gr.Row(): # 좌: 입력 with gr.Column(scale=1): Q_in = gr.Number(label="유량 Q (m³/s)", value=2.0) rain = gr.Number(label="강수량 (mm/24h)", value=10.0) dp = gr.Number(label="트래시랙 (kPa)", value=1.0) soc = gr.Number(label="SoC (%)", value=50.0) run_btn = gr.Button("▶ 실행", variant="primary") # 가운데: 즉시 실행 패널 with gr.Column(scale=1, elem_classes=["quick-col"]): gr.Markdown("
⚡ 즉시 실행
") btn_norm = gr.Button("정상", variant="secondary") btn_low = gr.Button("저유량", variant="secondary") btn_high = gr.Button("고유량", variant="secondary") btn_maint = gr.Button("정비", variant="stop") gr.Markdown("
🎬 자동 시연
") demo_btn = gr.Button("자동 시연", variant="primary") # 우: 출력 (자연스럽게 오른쪽으로 이동) with gr.Column(scale=3): out_vis = gr.HTML(elem_classes=["wrap-panel"]) out_md = gr.Markdown() out_extra = gr.HTML() # 이벤트 바인딩 run_btn.click(fn=run, inputs=[Q_in, rain, dp, soc], outputs=[out_md, out_vis, out_extra]) btn_norm.click(fn=preset_normal_and_run, outputs=[Q_in, rain, dp, soc, out_md, out_vis, out_extra]) btn_low.click(fn=preset_low_and_run, outputs=[Q_in, rain, dp, soc, out_md, out_vis, out_extra]) btn_high.click(fn=preset_high_and_run, outputs=[Q_in, rain, dp, soc, out_md, out_vis, out_extra]) btn_maint.click(fn=preset_maint_and_run,outputs=[Q_in, rain, dp, soc, out_md, out_vis, out_extra]) demo_btn.click(fn=autoplay, outputs=[Q_in, rain, dp, soc, out_md, out_vis, out_extra]) if __name__ == "__main__": app.launch()