naohiro701 commited on
Commit
6f219ee
·
verified ·
1 Parent(s): 1a3358f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -77
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
- vc_ut = 0.0
 
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
- return {"lmp_df": lmp_df, "res_price_df": res_df, "dispatch_df": dispatch_df, "flows_df": flows_df}
 
 
 
 
 
 
 
 
 
 
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
- usd_jpy0 = st.number_input("USD/JPY (scalar)", value=148.21, step=0.5)
666
- lng_px0 = st.number_input("LNG (USD/MMBtu, scalar)", value=11.27, step=0.1)
667
- coal_px0 = st.number_input("Coal (USD/ton, scalar)", value=130.0, step=1.0)
668
- oil_px0 = st.number_input("Oil (USD/bbl, scalar)", value=80.0, step=1.0)
669
- nuc_vc0 = st.number_input("Nuclear var cost (JPY/MWh, scalar)", value=2300.0, step=100.0)
670
- rr0 = st.number_input("Reserve ratio (scalar)", value=0.03, step=0.005, min_value=0.0, max_value=0.2, format="%.3f")
671
- voll0 = st.number_input("VOLL (JPY/MWh, scalar)", value=300000.0, step=10000.0)
 
 
672
 
673
  st.header("VOM adder [JPY/MWh]")
674
- vom_df = st.data_editor(pd.DataFrame({
675
- "tech": ["lng","coal","oil","nuclear"],
676
- "VOM": [400.0, 600.0, 1000.0, 800.0]
677
- }), use_container_width=True)
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=1e-6
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")