Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -522,7 +522,8 @@ def clear_market_network(df_units: pd.DataFrame,
|
|
| 522 |
vom_u = float(vom_adders_map.get(tech_u, 0.0))
|
| 523 |
for t in range(T):
|
| 524 |
if row_u["is_renew"]:
|
| 525 |
-
|
|
|
|
| 526 |
elif fuel_u == "nuclear":
|
| 527 |
vc_ut = float(nuc_vc[t]) + vom_u
|
| 528 |
else:
|
|
@@ -622,7 +623,17 @@ def clear_market_network(df_units: pd.DataFrame,
|
|
| 622 |
})
|
| 623 |
flows_df = pd.DataFrame(flow_rows)
|
| 624 |
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
|
| 628 |
# -----------------------------
|
|
@@ -662,82 +673,22 @@ with st.sidebar:
|
|
| 662 |
uploaded = st.file_uploader("Time-varying overrides CSV(任意)", type=["csv"])
|
| 663 |
|
| 664 |
st.header("スカラー既定(CSV欠損の補完に使用)")
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
|
|
|
|
|
|
| 672 |
|
| 673 |
st.header("VOM adder [JPY/MWh]")
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
st.header("ユニット数(各エリア)")
|
| 680 |
-
units_df = st.data_editor(pd.DataFrame({
|
| 681 |
-
"region": regions,
|
| 682 |
-
"lng_units":[6,10,25,10,4,20,8,4,10,0],
|
| 683 |
-
"coal_units":[3,6,10,6,3,10,5,2,7,0],
|
| 684 |
-
"oil_units":[2,3,6,3,2,5,3,2,3,0],
|
| 685 |
-
"nuc_units":[0,2,4,2,0,3,1,0,2,0],
|
| 686 |
-
"solar_units":[20,30,60,30,12,40,20,12,30,0],
|
| 687 |
-
"on_units":[10,15,25,15,6,20,10,6,15,0],
|
| 688 |
-
"off_units":[2,3,6,3,1,4,2,1,3,0],
|
| 689 |
-
"river_units":[5,8,12,8,4,12,6,3,8,0]
|
| 690 |
-
}), use_container_width=True)
|
| 691 |
-
|
| 692 |
-
st.header("容量レンジ [MW/ユニット]")
|
| 693 |
-
cap_bounds = {
|
| 694 |
-
"lng": (200.0, 900.0), "coal": (300.0, 1000.0), "oil": (100.0, 700.0), "nuclear": (500.0, 1400.0)
|
| 695 |
-
}
|
| 696 |
-
ren_bounds = {
|
| 697 |
-
"solar": (10.0, 200.0), "onshore_wind": (20.0, 300.0),
|
| 698 |
-
"offshore_wind": (100.0, 600.0), "river": (10.0, 200.0)
|
| 699 |
-
}
|
| 700 |
-
|
| 701 |
-
st.header("熱率レンジ(GJ/MWh, 乱数生成)")
|
| 702 |
-
hr_bounds = {
|
| 703 |
-
"lng": (6.2, 6.9), "coal": (7.8, 9.0), "oil": (8.8, 10.0), "nuclear": (np.nan, np.nan)
|
| 704 |
-
}
|
| 705 |
-
|
| 706 |
-
st.header("最低出力(比率レンジ, 乱数生成)")
|
| 707 |
-
minout_cfg = {"lng": (0.0, 0.2), "coal": (0.2, 0.6), "oil": (0.0, 0.4), "nuclear": (0.6, 0.9)}
|
| 708 |
-
|
| 709 |
-
st.header("可用性(FOR, 平均停止時間[h])")
|
| 710 |
-
for_map = st.data_editor(pd.DataFrame({
|
| 711 |
-
"fuel": ["lng","coal","oil","nuclear"], "FOR":[0.06,0.08,0.10,0.04], "mean_down_h":[24,48,24,120]
|
| 712 |
-
}), use_container_width=True)
|
| 713 |
-
|
| 714 |
-
st.header("ランプ比率(capの何倍/時)")
|
| 715 |
-
ramp_df = st.data_editor(pd.DataFrame({
|
| 716 |
-
"fuel": ["lng","coal","oil","nuclear"], "RU_frac":[0.50,0.20,0.30,0.05], "RD_frac":[0.50,0.20,0.30,0.05]
|
| 717 |
-
}), use_container_width=True)
|
| 718 |
-
|
| 719 |
-
# Build config dicts
|
| 720 |
-
counts_cfg = {}
|
| 721 |
-
for _, row in units_df.iterrows():
|
| 722 |
-
rname = row["region"]
|
| 723 |
-
counts_cfg[(rname, "lng")] = int(row["lng_units"])
|
| 724 |
-
counts_cfg[(rname, "coal")] = int(row["coal_units"])
|
| 725 |
-
counts_cfg[(rname, "oil")] = int(row["oil_units"])
|
| 726 |
-
counts_cfg[(rname, "nuclear")] = int(row["nuc_units"])
|
| 727 |
-
counts_cfg[(rname, "solar")] = int(row["solar_units"])
|
| 728 |
-
counts_cfg[(rname, "onshore_wind")] = int(row["on_units"])
|
| 729 |
-
counts_cfg[(rname, "offshore_wind")] = int(row["off_units"])
|
| 730 |
-
counts_cfg[(rname, "river")] = int(row["river_units"])
|
| 731 |
-
|
| 732 |
-
ramp_up_frac_map = {row["fuel"]: float(row["RU_frac"]) for _, row in ramp_df.iterrows()}
|
| 733 |
-
ramp_down_frac_map = {row["fuel"]: float(row["RD_frac"]) for _, row in ramp_df.iterrows()}
|
| 734 |
-
for_map_dict = {row["fuel"]: float(row["FOR"]) for _, row in for_map.iterrows()}
|
| 735 |
-
mean_down_map = {row["fuel"]: float(row["mean_down_h"]) for _, row in for_map.iterrows()}
|
| 736 |
-
|
| 737 |
-
# Time-varying series (override CSV or synthesized)
|
| 738 |
-
if uploaded is not None:
|
| 739 |
-
ov = load_overrides_csv(uploaded, ts_slice.index)
|
| 740 |
-
usd_jpy_s = ov.get("usd_jpy", pd.Series(usd_jpy0, index=ts_slice.index))
|
| 741 |
lng_s = ov.get("lng_usd_per_mmbtu", pd.Series(lng_px0, index=ts_slice.index))
|
| 742 |
coal_s = ov.get("coal_usd_per_ton", pd.Series(coal_px0, index=ts_slice.index))
|
| 743 |
oil_s = ov.get("oil_usd_per_bbl", pd.Series(oil_px0, index=ts_slice.index))
|
|
@@ -788,7 +739,7 @@ if st.button("シミュレーション実行(ネットワーク&実務スケ
|
|
| 788 |
ramp_down_frac_map=ramp_down_frac_map,
|
| 789 |
intertie_caps_mw=intertie_caps,
|
| 790 |
vom_adders_map=vom_adders_map,
|
| 791 |
-
spill_penalty=
|
| 792 |
)
|
| 793 |
|
| 794 |
st.subheader("LMP(JPY/MWh)")
|
|
@@ -817,6 +768,40 @@ if st.button("シミュレーション実行(ネットワーク&実務スケ
|
|
| 817 |
st.subheader("平均連系潮流(MW)")
|
| 818 |
st.dataframe(avg_flow)
|
| 819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
# Downloads
|
| 821 |
csv1 = StringIO(); fleet_df.to_csv(csv1, index=False, encoding="utf-8")
|
| 822 |
st.download_button("ユニット一覧CSV", data=csv1.getvalue(), file_name="fleet_units.csv", mime="text/csv")
|
|
|
|
| 522 |
vom_u = float(vom_adders_map.get(tech_u, 0.0))
|
| 523 |
for t in range(T):
|
| 524 |
if row_u["is_renew"]:
|
| 525 |
+
# 再エネにも小さな可変費(運転費)を与えて価格ゼロ固定化を回避
|
| 526 |
+
vc_ut = vom_u
|
| 527 |
elif fuel_u == "nuclear":
|
| 528 |
vc_ut = float(nuc_vc[t]) + vom_u
|
| 529 |
else:
|
|
|
|
| 623 |
})
|
| 624 |
flows_df = pd.DataFrame(flow_rows)
|
| 625 |
|
| 626 |
+
# Build spill/shed DataFrames
|
| 627 |
+
spill_mat = np.zeros((T, len(regions_list)))
|
| 628 |
+
shed_mat = np.zeros((T, len(regions_list)))
|
| 629 |
+
for j, r_name in enumerate(regions_list):
|
| 630 |
+
for t in range(T):
|
| 631 |
+
spill_mat[t, j] = spill[(r_name, int(t))].value()
|
| 632 |
+
shed_mat[t, j] = shed[(r_name, int(t))].value()
|
| 633 |
+
spill_df = pd.DataFrame(spill_mat, index=times, columns=regions_list)
|
| 634 |
+
shed_df = pd.DataFrame(shed_mat, index=times, columns=regions_list)
|
| 635 |
+
|
| 636 |
+
return {"lmp_df": lmp_df, "res_price_df": res_df, "dispatch_df": dispatch_df, "flows_df": flows_df, "spill_df": spill_df, "shed_df": shed_df}
|
| 637 |
|
| 638 |
|
| 639 |
# -----------------------------
|
|
|
|
| 673 |
uploaded = st.file_uploader("Time-varying overrides CSV(任意)", type=["csv"])
|
| 674 |
|
| 675 |
st.header("スカラー既定(CSV欠損の補完に使用)")
|
| 676 |
+
usd_jpy0 = st.number_input("USD/JPY (scalar)", value=148.21, step=0.5)
|
| 677 |
+
lng_px0 = st.number_input("LNG (USD/MMBtu, scalar)", value=11.27, step=0.1)
|
| 678 |
+
coal_px0 = st.number_input("Coal (USD/ton, scalar)", value=130.0, step=1.0)
|
| 679 |
+
oil_px0 = st.number_input("Oil (USD/bbl, scalar)", value=80.0, step=1.0)
|
| 680 |
+
nuc_vc0 = st.number_input("Nuclear var cost (JPY/MWh, scalar)", value=2300.0, step=100.0)
|
| 681 |
+
rr0 = st.number_input("Reserve ratio (scalar)", value=0.03, step=0.005, min_value=0.0, max_value=0.2, format="%.3f")
|
| 682 |
+
voll0 = st.number_input("VOLL (JPY/MWh, scalar)", value=300000.0, step=10000.0)
|
| 683 |
+
# 新規: スピル罰金(オーバーサプライ時の限界価格を規定)
|
| 684 |
+
spill_penalty_ui = st.number_input("Spill penalty (JPY/MWh)", value=1.0, step=1.0, min_value=0.0)
|
| 685 |
|
| 686 |
st.header("VOM adder [JPY/MWh]")
|
| 687 |
+
vom_df = st.data_editor(pd.DataFrame({
|
| 688 |
+
"tech": ["lng","coal","oil","nuclear","solar","onshore_wind","offshore_wind","river"],
|
| 689 |
+
# 既定は小さめの値(0~1000JPY/MWh程度): 実勢VOMは別途CSVで上書き可能
|
| 690 |
+
"VOM": [400.0, 600.0, 1000.0, 800.0, 50.0, 80.0, 120.0, 30.0]
|
| 691 |
+
}), use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
lng_s = ov.get("lng_usd_per_mmbtu", pd.Series(lng_px0, index=ts_slice.index))
|
| 693 |
coal_s = ov.get("coal_usd_per_ton", pd.Series(coal_px0, index=ts_slice.index))
|
| 694 |
oil_s = ov.get("oil_usd_per_bbl", pd.Series(oil_px0, index=ts_slice.index))
|
|
|
|
| 739 |
ramp_down_frac_map=ramp_down_frac_map,
|
| 740 |
intertie_caps_mw=intertie_caps,
|
| 741 |
vom_adders_map=vom_adders_map,
|
| 742 |
+
spill_penalty=spill_penalty_ui
|
| 743 |
)
|
| 744 |
|
| 745 |
st.subheader("LMP(JPY/MWh)")
|
|
|
|
| 768 |
st.subheader("平均連系潮流(MW)")
|
| 769 |
st.dataframe(avg_flow)
|
| 770 |
|
| 771 |
+
# Diagnostics: 低LMPの原因解析
|
| 772 |
+
st.subheader("Diagnostics: LMPが極端に低い/ゼロになる要因")
|
| 773 |
+
spill_df = res["spill_df"]; shed_df = res["shed_df"]; lmp_df = res["lmp_df"]
|
| 774 |
+
area = st.selectbox("エリア選択(診断)", regions, index=2)
|
| 775 |
+
|
| 776 |
+
diag_df = pd.DataFrame({
|
| 777 |
+
"LMP": lmp_df[area],
|
| 778 |
+
"Spill_MW": spill_df[area],
|
| 779 |
+
"Shed_MW": shed_df[area]
|
| 780 |
+
})
|
| 781 |
+
st.plotly_chart(px.scatter(diag_df, x="Spill_MW", y="LMP", title=f"Spill vs LMP — {area}"), use_container_width=True)
|
| 782 |
+
st.plotly_chart(px.scatter(diag_df, x="Shed_MW", y="LMP", title=f"Shed vs LMP — {area}"), use_container_width=True)
|
| 783 |
+
|
| 784 |
+
# 再エネ余裕(未使用キャパ)とLMPの関係
|
| 785 |
+
disp = res["dispatch_df"]
|
| 786 |
+
units_ren = fleet_df[fleet_df["is_renew"]]
|
| 787 |
+
disp_merge = disp.merge(units_ren[["unit_id","region","tech","cap_MW","cf_key"]], on=["unit_id","region"], how="inner")
|
| 788 |
+
def _cf_lookup(r):
|
| 789 |
+
try:
|
| 790 |
+
return float(ts_slice.loc[r["Time"], r["cf_key"]])
|
| 791 |
+
except Exception:
|
| 792 |
+
return 0.0
|
| 793 |
+
disp_merge["cf"] = disp_merge.apply(_cf_lookup, axis=1)
|
| 794 |
+
disp_merge["avail"] = disp_merge["cap_MW"] * disp_merge["cf"]
|
| 795 |
+
disp_merge["slack"] = (disp_merge["avail"] - disp_merge["g_MW"]).clip(lower=0.0)
|
| 796 |
+
slack = disp_merge.groupby(["Time","region"], as_index=False)["slack"].sum().pivot(index="Time", columns="region", values="slack").reindex(columns=regions)
|
| 797 |
+
st.plotly_chart(px.scatter(x=slack[area], y=lmp_df[area], labels={"x":"Renewable Slack (MW)", "y":"LMP"}, title=f"Renewable Slack vs LMP — {area}"), use_container_width=True)
|
| 798 |
+
|
| 799 |
+
# メトリクス
|
| 800 |
+
eps = 1e-3
|
| 801 |
+
spill_hours = int((spill_df[area] > 1.0).sum())
|
| 802 |
+
near_spill_price_hours = int((((lmp_df[area] - spill_penalty_ui).abs() < eps) & (spill_df[area] > 1.0)).sum())
|
| 803 |
+
st.info(f"{area}: spill>1MW 時間数 = {spill_hours} / {len(spill_df)}、 かつ |LMP - spill_penalty|< {eps} の時間数 = {near_spill_price_hours}")
|
| 804 |
+
|
| 805 |
# Downloads
|
| 806 |
csv1 = StringIO(); fleet_df.to_csv(csv1, index=False, encoding="utf-8")
|
| 807 |
st.download_button("ユニット一覧CSV", data=csv1.getvalue(), file_name="fleet_units.csv", mime="text/csv")
|