""" 麦麦智农 - 基于 PCSE/WOFOST 的作物生长模拟平台 """ import streamlit as st import plotly.graph_objects as go from simulator import ( run_wofost, run_multi_crop, list_available_provinces, list_available_crops, CROP_META, ) st.set_page_config( page_title="麦麦智农", page_icon="🌾", layout="wide", initial_sidebar_state="expanded", ) def add_milestone_line(fig, x, color: str, text: str): """手动添加竖线标记,绕过 Plotly add_vline 对 datetime.date 的 bug。""" fig.add_shape( type="line", x0=x, x1=x, y0=0, y1=1, xref="x", yref="paper", line=dict(color=color, width=1.5, dash="dot"), ) fig.add_annotation( x=x, y=1.02, xref="x", yref="paper", text=text, showarrow=False, font=dict(color=color, size=10), bgcolor="rgba(255,255,255,0.8)", borderpad=2, ) # ─── Sidebar ──────────────────────────────────────────────────────────────── with st.sidebar: st.header("🌾 麦麦智农") st.caption("基于 PCSE/WOFOST 的真实作物模拟") st.divider() province = st.selectbox("省份", list_available_provinces()) year = st.selectbox("年份", [2019, 2020, 2021, 2022, 2023], index=3) crops = list_available_crops(province) crop_options = {c["crop_no"]: f"{c['emoji']} {c['name']}" for c in crops} selected_crop_no = st.selectbox( "作物", list(crop_options.keys()), format_func=lambda k: crop_options[k] ) mode = st.radio( "生产模式", ["pp", "wlp"], format_func=lambda m: {"pp": "潜在生产 (PP)", "wlp": "水分限制生产 (WLP)"}[m], ) area = st.number_input( "种植面积 (公顷)", min_value=0.1, max_value=10000.0, value=100.0, step=10.0 ) # ─── Run Simulation ───────────────────────────────────────────────────────── @st.cache_data(show_spinner=False) def cached_run(province, crop_no, year, mode): return run_wofost(province=province, crop_no=crop_no, year=year, mode=mode) @st.cache_data(show_spinner=False) def cached_multi_crop(province, year, mode): return run_multi_crop(province=province, year=year, mode=mode) with st.spinner("正在运行 WOFOST 作物模拟,请稍候..."): result = cached_run(province, selected_crop_no, year, mode) multi_crop_result = cached_multi_crop(province, year, mode) meta = result["meta"] summary = result["summary"] df = result["df"] crop_info = CROP_META.get(selected_crop_no, {"name": "未知作物", "emoji": "🌱", "color": "#888"}) twso = summary["twso"] if summary["twso"] is not None else 0.0 tagp = summary["tagp"] if summary["tagp"] is not None else 0.0 max_lai = summary["max_lai"] if summary["max_lai"] is not None else 0.0 total_twso_tons = twso * area / 1000.0 # ─── Header ───────────────────────────────────────────────────────────────── st.title(f"{crop_info['emoji']} {crop_info['name']} 生长模拟") st.caption(f"{province} · {year} 年 · {meta['mode_label']} · 基于 WOFOST 7.2 真实作物模型") # ─── KPIs ─────────────────────────────────────────────────────────────────── k1, k2, k3, k4 = st.columns(4) k1.metric("经济产量 (TWSO)", f"{twso:,.0f} kg/ha") k2.metric("总生物量 (TAGP)", f"{tagp:,.0f} kg/ha") k3.metric("最大叶面积指数 (LAI)", f"{max_lai:.2f}") k4.metric(f"{area:.0f} 公顷总产量", f"{total_twso_tons:,.1f} 吨") st.divider() # ─── Charts Row 1 ─────────────────────────────────────────────────────────── chart_left, chart_right = st.columns(2) with chart_left: st.subheader("📈 叶面积指数 (LAI) 动态") if not df.empty and "LAI" in df.columns: fig_lai = go.Figure() fig_lai.add_trace( go.Scatter( x=df.index, y=df["LAI"], mode="lines", line=dict(color=crop_info["color"], width=2.5), fill="tozeroy", name="LAI", ) ) ms = summary.get("milestones", {}) if "flowering" in ms: add_milestone_line(fig_lai, x=ms["flowering"], color="#d4a574", text="开花") if "maturity" in ms: add_milestone_line(fig_lai, x=ms["maturity"], color="#7c5e42", text="成熟") fig_lai.update_layout( xaxis_title="日期", yaxis_title="LAI", margin=dict(t=20, b=36, l=50, r=20), height=300, showlegend=False, ) st.plotly_chart(fig_lai, use_container_width=True) else: st.info("暂无 LAI 数据") with chart_right: st.subheader("📊 生物量与产量积累") if not df.empty and "TAGP" in df.columns: fig_bio = go.Figure() fig_bio.add_trace( go.Scatter( x=df.index, y=df["TAGP"], mode="lines", name="TAGP", line=dict(color="#7a9e7e", width=2.5) ) ) if "TWSO" in df.columns: fig_bio.add_trace( go.Scatter( x=df.index, y=df["TWSO"], mode="lines", name="TWSO", line=dict(color=crop_info["color"], width=2.5), ) ) ms = summary.get("milestones", {}) if "flowering" in ms: add_milestone_line(fig_bio, x=ms["flowering"], color="#d4a574", text="开花") if "maturity" in ms: add_milestone_line(fig_bio, x=ms["maturity"], color="#7c5e42", text="成熟") fig_bio.update_layout( xaxis_title="日期", yaxis_title="干物质 (kg/ha)", margin=dict(t=20, b=36, l=50, r=20), height=300, legend=dict(orientation="h", y=-0.18), ) st.plotly_chart(fig_bio, use_container_width=True) else: st.info("暂无生物量数据") # ─── Charts Row 2 ─────────────────────────────────────────────────────────── chart2_left, chart2_right = st.columns(2) with chart2_left: st.subheader("💧 土壤水分动态") if not df.empty and "SM" in df.columns: fig_sm = go.Figure() fig_sm.add_trace( go.Scatter( x=df.index, y=df["SM"], mode="lines", line=dict(color="#5a8f9e", width=2.5), fill="tozeroy", name="SM", ) ) fig_sm.update_layout( xaxis_title="日期", yaxis_title="土壤含水量 (cm³/cm³)", margin=dict(t=20, b=36, l=50, r=20), height=280, showlegend=False, ) st.plotly_chart(fig_sm, use_container_width=True) else: st.info("暂无土壤水分数据") with chart2_right: st.subheader("🏅 同一年份作物产量对比") if multi_crop_result: names = [ f"{CROP_META.get(r['crop_no'], {}).get('emoji', '🌱')} {r['name']}" for r in multi_crop_result ] twsos = [r["twso"] if r["twso"] is not None else 0.0 for r in multi_crop_result] colors = [CROP_META.get(r["crop_no"], {}).get("color", "#888") for r in multi_crop_result] fig_bar = go.Figure() fig_bar.add_trace( go.Bar( x=names, y=twsos, marker=dict(color=colors, opacity=0.85), text=[f"{v:,.0f}" for v in twsos], textposition="outside", ) ) fig_bar.update_layout( yaxis_title="经济产量 (kg/ha)", margin=dict(t=20, b=30, l=60, r=20), height=280, showlegend=False, ) st.plotly_chart(fig_bar, use_container_width=True) else: st.info("暂无对比数据") # ─── Advisory Panel ───────────────────────────────────────────────────────── st.divider() st.subheader("💡 农艺建议与生育期") adv1, adv2 = st.columns(2) with adv1: if twso > 6000: st.success( f"{crop_info['name']} 模拟产量达到 {twso:,.0f} kg/ha,属于高产水平,气候与土壤条件匹配良好。" ) elif twso > 3000: st.info( f"{crop_info['name']} 模拟产量为 {twso:,.0f} kg/ha,处于中等水平,可通过优化水肥管理进一步提升。" ) else: st.warning( f"{crop_info['name']} 模拟产量仅 {twso:,.0f} kg/ha,建议检查品种适宜性或水分胁迫情况。" ) if mode == "wlp": if not df.empty and "SM" in df.columns: sm_min = df["SM"].min() if sm_min < 0.15: st.warning( f"模拟期间土壤水分最低降至 {sm_min:.3f},出现明显水分胁迫,建议评估灌溉方案。" ) else: st.success("模拟期间土壤水分状况总体良好,未出现极端干旱胁迫。") else: st.info("当前为潜在生产模式,结果反映理想水肥条件下的产量上限。") if max_lai > 5: st.success(f"最大 LAI 达到 {max_lai:.2f},冠层覆盖充分,光能截获效率高。") elif max_lai < 2: st.warning(f"最大 LAI 仅 {max_lai:.2f},冠层发育不足,可能存在播期或品种问题。") with adv2: ms = summary.get("milestones", {}) if ms: st.info( f""" **生育期里程碑** - 播种/出苗:{ms.get('start', '—')} - 开花期:{ms.get('flowering', '—')} - 成熟期:{ms.get('maturity', '—')} - 收获/结束:{ms.get('end', '—')} - 生育期天数:{summary.get('duration', '—')} 天 """ ) else: st.info("暂无生育期数据") st.divider() st.caption("麦麦智农 · 基于 PCSE/WOFOST 真实作物生长模型 · 结果仅供参考")