Files
maimai-pcse/app.py
zhenghu f27801e36b fix: 绕过 Plotly add_vline 对 datetime.date 的兼容性问题
手动通过 add_shape + add_annotation 添加里程碑竖线标记,
  替换 LAI 和生物量图表中原有的 add_vline 调用。
2026-04-14 15:55:02 +08:00

288 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
麦麦智农 - 基于 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 真实作物生长模型 · 结果仅供参考")