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()