提交正文:
- simulator.py: 新增 SyntheticWeatherDataProvider,基于河南、黑龙江、湖北、
新疆、四川五省气候模板生成合成日气象数据
- simulator.py: 引入国内农事日历 CROP_CALENDAR_CN 与省份作物配置 PROVINCE_CROPS
- simulator.py: 移除对 GridWeatherDataProvider / AgroManagementDataProvider /
fetch_sitedata 的西班牙 Demo 数据依赖
- app.py: 侧边栏支持省份选择,年份范围扩展为 2019-2023
- app.py: 全面移除自定义 CSS,改用 Streamlit 原生组件(st.metric / st.info /
st.success / st.divider 等)简化界面
- app.py: 图表回归 Plotly 原生 add_vline,移除 hex_to_rgba / add_milestone_line
辅助函数
279 lines
10 KiB
Python
279 lines
10 KiB
Python
"""
|
||
麦麦智农 - 基于 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",
|
||
)
|
||
|
||
|
||
# ─── 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:
|
||
fig_lai.add_vline(
|
||
x=ms["flowering"],
|
||
line=dict(color="#d4a574", width=1.5, dash="dot"),
|
||
annotation_text="开花",
|
||
annotation_position="top left",
|
||
)
|
||
if "maturity" in ms:
|
||
fig_lai.add_vline(
|
||
x=ms["maturity"],
|
||
line=dict(color="#7c5e42", width=1.5, dash="dash"),
|
||
annotation_text="成熟",
|
||
annotation_position="top right",
|
||
)
|
||
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:
|
||
fig_bio.add_vline(x=ms["flowering"], line=dict(color="#d4a574", width=1.5, dash="dot"))
|
||
if "maturity" in ms:
|
||
fig_bio.add_vline(x=ms["maturity"], line=dict(color="#7c5e42", width=1.5, dash="dash"))
|
||
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 真实作物生长模型 · 结果仅供参考")
|