naohiro701 commited on
Commit
517cbd6
·
verified ·
1 Parent(s): 6f04a37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -297
app.py CHANGED
@@ -1,274 +1,258 @@
 
 
1
  import streamlit as st
2
- import requests
3
  import pandas as pd
4
- import pulp
5
- import plotly.graph_objs as go
6
  import plotly.express as px
 
 
7
  import numpy as np
8
  import json
 
9
 
10
- # Function to fetch renewable energy data
 
 
11
  def get_json():
12
  """
13
- open data.json
 
 
 
 
 
 
 
14
  """
15
- with open('data.json') as f:
16
- data = json.load(f)
17
- if not data:
18
- return None, "No data found."
19
-
20
- base_times = data[next(iter(data))]['x']
21
- result_df = pd.DataFrame({"Time": base_times})
22
-
23
- for energy_type, energy_data in data.items():
24
- if 'x' in energy_data and 'y' in energy_data:
25
- values = energy_data['y']
26
- result_df[f"{energy_type} hourly capacity factor"] = values
27
-
28
- return result_df
29
-
30
- # Function to optimize the energy system and create visualizations
31
- def optimize_energy_system(solar_cost, onshore_wind_cost, offshore_wind_cost, river_cost, battery_cost, yearly_demand, solar_range, wind_range, river_range, offshore_wind_range):
32
- data = get_json()
33
-
34
- for col in data.columns[1:]:
35
- data[col] = pd.to_numeric(data[col], errors='coerce')
36
- data = data.fillna(0)
37
-
38
- time_steps = range(len(data['Time']))
39
- solar_cf = data['solar hourly capacity factor']
40
- onshore_wind_cf = data['onshore_wind hourly capacity factor']
41
- offshore_wind_cf = data['offshore_wind hourly capacity factor']
42
- river_cf = data['river hourly capacity factor']
43
- demand_cf = data['demand hourly capacity factor']
44
-
45
- regions = ['region1']
46
- technologies = ['solar', 'onshore_wind', 'offshore_wind', 'river']
47
- capacity_factor = {
48
- 'solar': solar_cf,
49
- 'onshore_wind': onshore_wind_cf,
50
- 'offshore_wind': offshore_wind_cf,
51
- 'river': river_cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
-
54
- renewable_capacity_cost = {'solar': solar_cost, 'onshore_wind': onshore_wind_cost, 'offshore_wind': offshore_wind_cost, 'river': river_cost}
55
- battery_cost_per_mwh = battery_cost
56
- battery_efficiency = 0.9
57
-
58
- demand = demand_cf * yearly_demand / 100 * 1000 * 1000
59
-
60
- renewable_capacity = pulp.LpVariable.dicts("renewable_capacity",
61
- [(r, g) for r in regions for g in technologies],
62
- lowBound=0, cat='Continuous')
63
- curtailment = pulp.LpVariable.dicts("curtailment",
64
- [(r, t) for r in regions for t in time_steps],
65
- lowBound=0, cat='Continuous')
66
- battery_capacity = pulp.LpVariable("battery_capacity", lowBound=0, cat='Continuous')
67
- battery_charge = pulp.LpVariable.dicts("battery_charge", time_steps, lowBound=0, cat='Continuous')
68
- battery_discharge = pulp.LpVariable.dicts("battery_discharge", time_steps, lowBound=0, cat='Continuous')
69
- SOC = pulp.LpVariable.dicts("SOC", time_steps, lowBound=0, cat='Continuous')
70
-
71
- model = pulp.LpProblem("EnergySystemOptimizationWithBattery", pulp.LpMinimize)
72
-
73
- model += pulp.lpSum([renewable_capacity[(r, g)] * renewable_capacity_cost[g]
74
- for r in regions for g in technologies]) + \
75
- battery_capacity * battery_cost_per_mwh, "TotalCost"
76
-
77
- for r in regions:
78
- for t in time_steps:
79
- model += pulp.lpSum([renewable_capacity[(r, g)] * capacity_factor[g][t]
80
- for g in technologies]) + battery_discharge[t] == demand[t] + battery_charge[t] + curtailment[(r, t)], f"DemandConstraint_{r}_{t}"
81
-
82
- if t == 0:
83
- model += SOC[t] == battery_charge[t] * battery_efficiency - battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}"
84
- else:
85
- model += SOC[t] == SOC[t - 1] + battery_charge[t] * battery_efficiency - battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}"
86
-
87
- model += SOC[t] <= battery_capacity, f"SOCUpperBound_{t}"
88
-
89
- model += renewable_capacity[('region1', 'solar')] >= solar_range[0], "SolarMinConstraint"
90
- model += renewable_capacity[('region1', 'solar')] <= solar_range[1], "SolarMaxConstraint"
91
- model += renewable_capacity[('region1', 'onshore_wind')] >= wind_range[0], "WindMinConstraint"
92
- model += renewable_capacity[('region1', 'onshore_wind')] <= wind_range[1], "WindMaxConstraint"
93
- model += renewable_capacity[('region1', 'offshore_wind')] >= offshore_wind_range[0], "OffshoreWindMinConstraint"
94
- model += renewable_capacity[('region1', 'offshore_wind')] <= offshore_wind_range[1], "OffshoreWindMaxConstraint"
95
- model += renewable_capacity[('region1', 'river')] >= river_range[0], "RiverMinConstraint"
96
- model += renewable_capacity[('region1', 'river')] <= river_range[1], "RiverMaxConstraint"
97
-
98
- model.solve()
99
-
100
- supply_solar = solar_cf * renewable_capacity[('region1', 'solar')].varValue
101
- supply_onshore_wind = onshore_wind_cf * renewable_capacity[('region1', 'onshore_wind')].varValue
102
- supply_offshore_wind = offshore_wind_cf * renewable_capacity[('region1', 'offshore_wind')].varValue
103
- supply_river = river_cf * renewable_capacity[('region1', 'river')].varValue
104
-
105
- battery_discharge_values = [battery_discharge[t].varValue for t in time_steps]
106
- battery_charge_values = [-battery_charge[t].varValue for t in time_steps]
107
- SOC_values = [SOC[t].varValue for t in time_steps]
108
- curtailment_values = [-curtailment[(r, t)].varValue for r in regions for t in time_steps]
109
-
110
- max_SOC = max(SOC_values)
111
- SOC_normalized = [(soc / max_SOC) * 100 for soc in SOC_values] if max_SOC > 0 else [0] * len(SOC_values)
112
-
113
- fig_energy = go.Figure()
114
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=supply_solar, mode='lines', stackgroup='one', name='Solar', line=dict(color='#FFD700', width=0)))
115
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=supply_onshore_wind, mode='lines', stackgroup='one', name='Onshore Wind', line=dict(color='#1F78B4', width=0)))
116
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=supply_offshore_wind, mode='lines', stackgroup='one', name='Offshore Wind', line=dict(color='#66C2A5', width=0)))
117
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=supply_river, mode='lines', stackgroup='one', name='Run of River', line=dict(color='#FF7F00', width=0)))
118
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=battery_discharge_values, mode='lines', stackgroup='one', name='Battery Discharge', fill='tonexty', line=dict(color='#6A3D9A', width=0)))
119
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=battery_charge_values, mode='lines', stackgroup='two', name='Battery Charge', fill='tonexty', line=dict(color='#6A3D9A', width=0)))
120
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=-demand, mode='lines', stackgroup='two', name='Demand', line=dict(color='black', width=0)))
121
- fig_energy.add_trace(go.Scatter(x=data['Time'], y=curtailment_values, mode='lines', stackgroup='two', name='Curtailment', line=dict(color='#aaaaaa', width=0)))
122
-
123
- fig_energy.update_layout(
124
- title_text='Power Supply and Demand',
125
- title_x=0.5,
126
- yaxis_title='Power dispatch (MW)',
127
- legend_title='Source',
128
- font=dict(size=12),
129
- margin=dict(l=40, r=40, t=40, b=40),
130
- hovermode='x unified',
131
- plot_bgcolor='white',
132
- xaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'),
133
- yaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray')
134
  )
135
-
136
- # Heatmap generation for each renewable energy source
137
- heatmaps = []
138
- for energy_source in ['solar', 'onshore_wind', 'offshore_wind', 'river']:
139
- df_heatmap = data[['Time', f'{energy_source} hourly capacity factor']].copy()
140
- df_heatmap['Time'] = pd.to_datetime(df_heatmap['Time'], errors='coerce')
141
- df_heatmap['day_of_year'] = df_heatmap['Time'].dt.dayofyear
142
- df_heatmap['hour_of_day'] = df_heatmap['Time'].dt.hour
143
-
144
- pivot_df = df_heatmap.pivot_table(
145
- index='hour_of_day',
146
- columns='day_of_year',
147
- values=f'{energy_source} hourly capacity factor',
148
- aggfunc='mean'
149
- )
150
-
151
- fig_heatmap = px.imshow(
152
- pivot_df.values,
153
- labels=dict(x="Day of Year", y="Hour of Day", color=f"{energy_source.replace('_', ' ').title()} Capacity Factor"),
154
- x=pivot_df.columns,
155
- y=pivot_df.index,
156
- aspect="auto",
157
- color_continuous_scale='Plasma'
158
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- fig_heatmap.update_layout(
161
- title=f'{energy_source.replace("_", " ").title()} Hourly Capacity Factor (24 Hours x 365 Days)',
162
- xaxis_title='Day of Year',
163
- yaxis_title='Hour of Day',
164
- font=dict(size=12),
165
- plot_bgcolor='white',
166
- margin=dict(l=40, r=40, t=40, b=40),
167
- )
168
- heatmaps.append(fig_heatmap)
169
-
170
- # Create capacity range visualization for each technology
171
- fig_capacity_ranges = go.Figure()
172
- technologies = ['solar', 'onshore_wind', 'offshore_wind', 'river']
173
- capacity_ranges = [solar_range, wind_range, offshore_wind_range, river_range]
174
- optimized_capacities = [
175
- renewable_capacity[('region1', 'solar')].varValue,
176
- renewable_capacity[('region1', 'onshore_wind')].varValue,
177
- renewable_capacity[('region1', 'offshore_wind')].varValue,
178
- renewable_capacity[('region1', 'river')].varValue
179
- ]
180
-
181
- for tech, cap_range, optimized_cap in zip(technologies, capacity_ranges, optimized_capacities):
182
- fig_capacity_ranges.add_trace(go.Scatter(
183
- x=[tech, tech],
184
- y=cap_range,
185
- mode='lines',
186
- name=f'{tech} capacity range',
187
- line=dict(color='blue', width=4)
188
- ))
189
- fig_capacity_ranges.add_trace(go.Scatter(
190
- x=[tech],
191
- y=[optimized_cap],
192
- mode='markers',
193
- name=f'{tech} optimized capacity',
194
- marker=dict(color='red', symbol='x', size=10)
195
- ))
196
-
197
- fig_capacity_ranges.update_layout(
198
- title_text='Optimized Capacity vs. Capacity Ranges',
199
- title_x=0.5,
200
- yaxis_title='Capacity (MW)',
201
- xaxis_title='Technology',
202
- font=dict(size=12),
203
- margin=dict(l=40, r=40, t=40, b=40),
204
- hovermode='x unified',
205
- plot_bgcolor='white',
206
- xaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'),
207
- yaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray')
208
- )
209
 
210
- return fig_energy, heatmaps, curtailment_values, SOC_normalized, fig_capacity_ranges, renewable_capacity
211
-
212
- # 資源コストの感度解析を行う関数
213
- def analyze_cost_sensitivity(renewable_capacity_cost, technologies, renewable_capacity):
214
- # コストの変動範囲(0.5倍から1.5倍)
215
- cost_multipliers = np.linspace(0.5, 1.5, 11)
216
-
217
- # 結果を格納する辞書
218
- sensitivity_results = {}
219
-
220
- for tech in technologies:
221
- # 各技術ごとのコスト変動に対する総コストの変化を計算
222
- original_cost = renewable_capacity_cost[tech]
223
- total_costs = []
224
-
225
- for multiplier in cost_multipliers:
226
- # コストを変更
227
- modified_cost = original_cost * multiplier
228
- # 総コスト = 変更後���コスト * 設備容量
229
- total_cost = modified_cost * renewable_capacity[('region1', tech)].varValue
230
- total_costs.append(total_cost)
231
-
232
- # 技術ごとに結果を保存
233
- sensitivity_results[tech] = total_costs
234
-
235
- # 可視化
236
- fig = go.Figure()
237
-
238
- for tech, total_costs in sensitivity_results.items():
239
- fig.add_trace(go.Scatter(
240
- x=cost_multipliers,
241
- y=total_costs,
242
- mode='lines+markers',
243
- name=f'{tech} Cost Sensitivity'
244
- ))
245
-
246
- # グラフのレイアウト
247
- fig.update_layout(
248
- title='Cost Sensitivity Analysis: Impact of Cost Changes on Total System Cost',
249
- xaxis_title='Cost Multiplier (0.5x to 1.5x)',
250
- yaxis_title='Total System Cost (¥)',
251
- hovermode='x unified',
252
- plot_bgcolor='white',
253
- xaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'),
254
- yaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray')
255
- )
256
 
257
- return fig
 
 
 
 
 
 
258
 
259
- # Streamlit UI setup
260
- st.set_page_config(page_title='Renewable Energy System Optimization', layout='wide')
261
- st.title('Renewable Energy System Optimization')
 
 
262
 
263
  st.markdown("""
264
- ### Model Overview
265
- This application is designed to optimize renewable energy systems for a specific region. The model allows the user to set the costs for different renewable energy technologies and battery storage, as well as minimum and maximum capacity limits for each technology. The optimization uses linear programming to minimize the total cost while ensuring demand is met, incorporating energy storage to help manage intermittency.
266
- The renewable technologies considered are:
267
- - Solar PV
268
- - Onshore Wind
269
- - Offshore Wind
270
- - Run of River (Hydro)
271
- The optimization problem aims to balance supply and demand at minimal cost, while also providing flexibility in the form of battery energy storage. Curtailment and battery state of charge are also considered in the model.
272
  """)
273
 
274
  with st.sidebar:
@@ -284,48 +268,44 @@ with st.sidebar:
284
  offshore_wind_range = st.slider("Offshore Wind Capacity Range (MW)", 0, 10000, (0, 10000))
285
  river_range = st.slider("River Capacity Range (MW)", 0, 10000, (0, 10000))
286
 
287
- calculated_optimal_energy_mix = False
 
288
 
289
- if st.button('Calculate Optimal Energy Mix'):
290
- fig_energy, heatmaps, curtailment_values, soc_per_hour, fig_capacity_ranges, renewable_capacity = optimize_energy_system(
291
- solar_cost, onshore_wind_cost, offshore_wind_cost, river_cost, battery_cost, yearly_demand, solar_range, wind_range, river_range, offshore_wind_range
292
- )
293
-
294
- if fig_energy:
295
- st.plotly_chart(fig_energy, use_container_width=True, height=800)
296
-
297
- # Additional visualizations
298
- st.markdown("### Hourly Capacity Factor Heatmaps")
299
- for fig_heatmap in heatmaps:
300
- st.plotly_chart(fig_heatmap, use_container_width=True, height=800)
301
-
302
- st.markdown("### Additional Analysis")
303
- st.markdown("The following plots provide additional insights into the renewable energy mix, curtailment, and electricity price variations.")
304
-
305
- # Plot curtailment over time
306
- curtailment_df = pd.DataFrame({"Time": fig_energy.data[0].x, "Curtailment (MW)": curtailment_values})
307
- fig_curtailment = px.line(curtailment_df, x='Time', y='Curtailment (MW)', title='Curtailment Over Time', template='plotly_white')
308
- st.plotly_chart(fig_curtailment, use_container_width=True, height=800)
309
-
310
- # Plot electricity price variation over time
311
- soc_df = pd.DataFrame({"Time": fig_energy.data[0].x, "State of charge [%]": soc_per_hour})
312
- fig_battery_operation = px.line(soc_df, x='Time', y='State of charge [%]', title='State of charge in battery', template='plotly_white')
313
- st.plotly_chart(fig_battery_operation, use_container_width=True, height=800)
314
-
315
- # Plot optimized capacity vs. capacity ranges
316
- st.plotly_chart(fig_capacity_ranges, use_container_width=True, height=800)
317
-
318
- calculated_optimal_energy_mix = True
319
-
320
- # Streamlit UIに感度解析ボタンを追加
321
- if st.button('Analyze Cost Sensitivity'):
322
- if calculated_optimal_energy_mix:
323
- fig_sensitivity = analyze_cost_sensitivity({
324
- 'solar': solar_cost,
325
- 'onshore_wind': onshore_wind_cost,
326
- 'offshore_wind': offshore_wind_cost,
327
- 'river': river_cost
328
- }, ['solar', 'onshore_wind', 'offshore_wind', 'river'], renewable_capacity)
329
- st.plotly_chart(fig_sensitivity, use_container_width=True, height=800)
330
- else:
331
- st.error("Please calculate the optimal energy mix first before running the cost sensitivity analysis.")
 
1
+ # app.py
2
+ # -*- coding: utf-8 -*-
3
  import streamlit as st
 
4
  import pandas as pd
 
 
5
  import plotly.express as px
6
+ import plotly.graph_objs as go
7
+ import pulp
8
  import numpy as np
9
  import json
10
+ from io import StringIO
11
 
12
+ # -----------------------------
13
+ # Data I/O
14
+ # -----------------------------
15
  def get_json():
16
  """
17
+ Open data.json and return a tidy DataFrame.
18
+ Columns expected (examples):
19
+ - 'Time'
20
+ - 'solar hourly capacity factor'
21
+ - 'onshore_wind hourly capacity factor'
22
+ - 'offshore_wind hourly capacity factor'
23
+ - 'river hourly capacity factor'
24
+ - 'demand hourly capacity factor'
25
  """
26
+ with open('data.json', 'r', encoding='utf-8') as f:
27
+ data_local = json.load(f)
28
+ if not data_local:
29
+ return None
30
+
31
+ base_times = data_local[next(iter(data_local))]['x']
32
+ df_local = pd.DataFrame({"Time": base_times})
33
+
34
+ for k_local, v_local in data_local.items():
35
+ if isinstance(v_local, dict) and 'x' in v_local and 'y' in v_local:
36
+ df_local[f"{k_local} hourly capacity factor"] = v_local['y']
37
+
38
+ # Coerce numeric + NA fill
39
+ for col_local in df_local.columns[1:]:
40
+ df_local[col_local] = pd.to_numeric(df_local[col_local], errors='coerce')
41
+ df_local = df_local.fillna(0.0)
42
+
43
+ return df_local
44
+
45
+ # -----------------------------
46
+ # Core LP builder/solver with duals
47
+ # -----------------------------
48
+ def optimize_with_duals(
49
+ solar_cost_local: float,
50
+ onshore_wind_cost_local: float,
51
+ offshore_wind_cost_local: float,
52
+ river_cost_local: float,
53
+ battery_cost_per_mwh_local: float,
54
+ yearly_demand_twh_local: float,
55
+ solar_range_local: tuple,
56
+ wind_range_local: tuple,
57
+ river_range_local: tuple,
58
+ offshore_wind_range_local: tuple,
59
+ battery_eff_local: float = 0.9
60
+ ):
61
+ """
62
+ Build and solve the capacity-planning LP with storage and extract duals.
63
+
64
+ Returns:
65
+ dict:
66
+ {
67
+ 'status': str,
68
+ 'objective': float,
69
+ 'df': pandas.DataFrame (input CFs + Time),
70
+ 'caps': dict of optimized capacities,
71
+ 'soc': list of SOC values,
72
+ 'charge': list of charge,
73
+ 'discharge': list of discharge,
74
+ 'curtail': list of curtailment,
75
+ 'lmp': pandas.Series of LMP (one value per time),
76
+ 'demand': numpy.ndarray of demand (MW)
77
+ }
78
+ Notes:
79
+ - All variables are continuous; duals (shadow prices) are available from CBC.
80
+ - LMP is taken as the (signed) dual of the demand-balance constraint per time.
81
+ We map it so that a positive value means: "Increasing demand by 1 MW raises total cost by that amount."
82
+ """
83
+ df_local = get_json()
84
+ if df_local is None:
85
+ raise RuntimeError("data.json not found or empty.")
86
+
87
+ # Time index
88
+ t_idx_local = list(range(len(df_local['Time'])))
89
+
90
+ # Capacity factors
91
+ cf_map_local = {
92
+ 'solar': df_local['solar hourly capacity factor'].astype(float).values,
93
+ 'onshore_wind': df_local['onshore_wind hourly capacity factor'].astype(float).values,
94
+ 'offshore_wind': df_local['offshore_wind hourly capacity factor'].astype(float).values,
95
+ 'river': df_local['river hourly capacity factor'].astype(float).values
96
  }
97
+ demand_cf_local = df_local['demand hourly capacity factor'].astype(float).values
98
+
99
+ # Demand level (MW): TWh/year -> MWh/year -> scale by hourly CF share
100
+ # Here demand_cf is a % share per hour that sums to ~100%*hours if provided as percentage.
101
+ # If it's in [0,1] and sums to 1 over the year, adjust accordingly as needed by your data design.
102
+ demand_mw_total_local = yearly_demand_twh_local * 1e6 # TWh -> MWh, then per hour sum will distribute via cf
103
+ # Assuming demand_cf_local sums to 100 over the year (percentage), divide by 100 to get shares.
104
+ demand_local = (demand_cf_local / 100.0) * demand_mw_total_local
105
+
106
+ # Model
107
+ mdl_local = pulp.LpProblem("EnergySystemOptimizationWithBattery", pulp.LpMinimize)
108
+
109
+ # Variables
110
+ caps_local = {
111
+ ('region1', 'solar'): pulp.LpVariable("cap_solar", lowBound=0, cat='Continuous'),
112
+ ('region1', 'onshore_wind'): pulp.LpVariable("cap_onshore", lowBound=0, cat='Continuous'),
113
+ ('region1', 'offshore_wind'): pulp.LpVariable("cap_offshore", lowBound=0, cat='Continuous'),
114
+ ('region1', 'river'): pulp.LpVariable("cap_river", lowBound=0, cat='Continuous')
115
+ }
116
+ cap_batt_local = pulp.LpVariable("cap_battery", lowBound=0, cat='Continuous')
117
+
118
+ chg_local = {t: pulp.LpVariable(f"charge_{t}", lowBound=0, cat='Continuous') for t in t_idx_local}
119
+ dch_local = {t: pulp.LpVariable(f"discharge_{t}", lowBound=0, cat='Continuous') for t in t_idx_local}
120
+ soc_local = {t: pulp.LpVariable(f"soc_{t}", lowBound=0, cat='Continuous') for t in t_idx_local}
121
+ curt_local = {t: pulp.LpVariable(f"curtail_{t}", lowBound=0, cat='Continuous') for t in t_idx_local}
122
+
123
+ # Objective: capacity costs only (as in your original)
124
+ cost_map_local = {
125
+ 'solar': solar_cost_local,
126
+ 'onshore_wind': onshore_wind_cost_local,
127
+ 'offshore_wind': offshore_wind_cost_local,
128
+ 'river': river_cost_local
129
+ }
130
+ obj_local = (
131
+ caps_local[('region1', 'solar')] * cost_map_local['solar'] +
132
+ caps_local[('region1', 'onshore_wind')] * cost_map_local['onshore_wind'] +
133
+ caps_local[('region1', 'offshore_wind')] * cost_map_local['offshore_wind'] +
134
+ caps_local[('region1', 'river')] * cost_map_local['river'] +
135
+ cap_batt_local * battery_cost_per_mwh_local
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  )
137
+ mdl_local += obj_local
138
+
139
+ # Capacity bounds
140
+ mdl_local += caps_local[('region1', 'solar')].ge(solar_range_local[0]), "SolarMin"
141
+ mdl_local += caps_local[('region1', 'solar')].le(solar_range_local[1]), "SolarMax"
142
+ mdl_local += caps_local[('region1', 'onshore_wind')].ge(wind_range_local[0]), "WindMin"
143
+ mdl_local += caps_local[('region1', 'onshore_wind')].le(wind_range_local[1]), "WindMax"
144
+ mdl_local += caps_local[('region1', 'offshore_wind')].ge(offshore_wind_range_local[0]), "OffshoreMin"
145
+ mdl_local += caps_local[('region1', 'offshore_wind')].le(offshore_wind_range_local[1]), "OffshoreMax"
146
+ mdl_local += caps_local[('region1', 'river')].ge(river_range_local[0]), "RiverMin"
147
+ mdl_local += caps_local[('region1', 'river')].le(river_range_local[1]), "RiverMax"
148
+
149
+ # Demand balance constraints per time (keep objects for duals)
150
+ bal_cons_local = {}
151
+ for t in t_idx_local:
152
+ gen_t_local = (
153
+ caps_local[('region1', 'solar')] * cf_map_local['solar'][t] +
154
+ caps_local[('region1', 'onshore_wind')] * cf_map_local['onshore_wind'][t] +
155
+ caps_local[('region1', 'offshore_wind')] * cf_map_local['offshore_wind'][t] +
156
+ caps_local[('region1', 'river')] * cf_map_local['river'][t]
 
 
 
157
  )
158
+ # Write as: gen + discharge - demand - charge - curtailment == 0
159
+ expr_local = gen_t_local + dch_local[t] - demand_local[t] - chg_local[t] - curt_local[t]
160
+ cons_local = pulp.LpConstraint(e=expr_local, sense=pulp.LpConstraintEQ, name=f"Balance_t{t}")
161
+ mdl_local += cons_local
162
+ bal_cons_local[t] = cons_local
163
+
164
+ # SOC and limits
165
+ if t == 0:
166
+ mdl_local += (soc_local[t] == chg_local[t] * battery_eff_local - dch_local[t] * (1.0 / battery_eff_local)), f"SOC_t{t}"
167
+ else:
168
+ mdl_local += (soc_local[t] == soc_local[t-1] + chg_local[t] * battery_eff_local - dch_local[t] * (1.0 / battery_eff_local)), f"SOC_t{t}"
169
+ mdl_local += (soc_local[t] <= cap_batt_local), f"SOCcap_t{t}"
170
+
171
+ # Solve (CBC)
172
+ solver_local = pulp.PULP_CBC_CMD(msg=False)
173
+ mdl_status_local = mdl_local.solve(solver_local)
174
+
175
+ # Extract solution
176
+ soc_vals_local = [pulp.value(soc_local[t]) for t in t_idx_local]
177
+ chg_vals_local = [pulp.value(chg_local[t]) for t in t_idx_local]
178
+ dch_vals_local = [pulp.value(dch_local[t]) for t in t_idx_local]
179
+ curt_vals_local = [pulp.value(curt_local[t]) for t in t_idx_local]
180
+ cap_dict_local = {
181
+ 'solar': pulp.value(caps_local[('region1', 'solar')]),
182
+ 'onshore_wind': pulp.value(caps_local[('region1', 'onshore_wind')]),
183
+ 'offshore_wind': pulp.value(caps_local[('region1', 'offshore_wind')]),
184
+ 'river': pulp.value(caps_local[('region1', 'river')]),
185
+ 'battery': pulp.value(cap_batt_local)
186
+ }
187
 
188
+ # Duals -> LMPs
189
+ # CBC returns duals for LPs; for equality of form (supply - demand == 0),
190
+ # the economically meaningful LMP (cost increase when demand increases) is -dual.
191
+ dual_raw_local = np.array([bal_cons_local[t].pi for t in t_idx_local], dtype=float)
192
+ lmp_local = -dual_raw_local # positive means "increase in cost for +1 MW of demand"
193
+
194
+ return {
195
+ 'status': pulp.LpStatus[mdl_local.status],
196
+ 'objective': float(pulp.value(mdl_local.objective)),
197
+ 'df': df_local,
198
+ 'caps': cap_dict_local,
199
+ 'soc': soc_vals_local,
200
+ 'charge': chg_vals_local,
201
+ 'discharge': dch_vals_local,
202
+ 'curtail': curt_vals_local,
203
+ 'lmp': pd.Series(lmp_local, index=df_local['Time'], name='LMP'),
204
+ 'demand': demand_local
205
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ # -----------------------------
208
+ # Visualization helpers
209
+ # -----------------------------
210
+ def make_supply_plot(df_local, cap_dict_local, charge_vals_local, discharge_vals_local, demand_local, curt_vals_local):
211
+ """
212
+ Build stacked supply-demand plot (Plotly).
213
+ """
214
+ time_local = df_local['Time']
215
+ sup_solar_local = df_local['solar hourly capacity factor'] * cap_dict_local['solar']
216
+ sup_on_local = df_local['onshore_wind hourly capacity factor'] * cap_dict_local['onshore_wind']
217
+ sup_off_local = df_local['offshore_wind hourly capacity factor'] * cap_dict_local['offshore_wind']
218
+ sup_riv_local = df_local['river hourly capacity factor'] * cap_dict_local['river']
219
+
220
+ fig_local = go.Figure()
221
+ fig_local.add_trace(go.Scatter(x=time_local, y=sup_solar_local, mode='lines', stackgroup='one', name='Solar', line=dict(width=0)))
222
+ fig_local.add_trace(go.Scatter(x=time_local, y=sup_on_local, mode='lines', stackgroup='one', name='Onshore', line=dict(width=0)))
223
+ fig_local.add_trace(go.Scatter(x=time_local, y=sup_off_local, mode='lines', stackgroup='one', name='Offshore', line=dict(width=0)))
224
+ fig_local.add_trace(go.Scatter(x=time_local, y=sup_riv_local, mode='lines', stackgroup='one', name='River', line=dict(width=0)))
225
+ fig_local.add_trace(go.Scatter(x=time_local, y=discharge_vals_local, mode='lines', stackgroup='one', name='Battery Discharge', line=dict(width=0)))
226
+ fig_local.add_trace(go.Scatter(x=time_local, y=-np.array(charge_vals_local), mode='lines', stackgroup='two', name='Battery Charge', line=dict(width=0)))
227
+ fig_local.add_trace(go.Scatter(x=time_local, y=-demand_local, mode='lines', stackgroup='two', name='Demand', line=dict(width=0)))
228
+ fig_local.add_trace(go.Scatter(x=time_local, y=-np.array(curt_vals_local), mode='lines', stackgroup='two', name='Curtailment', line=dict(width=0)))
229
+ fig_local.update_layout(title='Power Supply and Demand', yaxis_title='MW', hovermode='x unified', plot_bgcolor='white')
230
+ return fig_local
231
+
232
+ def make_lmp_fig(lmp_series_local: pd.Series):
233
+ """
234
+ Build LMP time-series chart.
235
+ """
236
+ fig_local = px.line(lmp_series_local.reset_index(), x='Time', y='LMP', title='LMP (dual of demand balance)')
237
+ fig_local.update_layout(hovermode='x unified', plot_bgcolor='white')
238
+ return fig_local
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ def make_lmp_hist(lmp_series_local: pd.Series):
241
+ """
242
+ Build LMP distribution chart.
243
+ """
244
+ fig_local = px.histogram(lmp_series_local, nbins=60, title='LMP distribution')
245
+ fig_local.update_layout(plot_bgcolor='white')
246
+ return fig_local
247
 
248
+ # -----------------------------
249
+ # Streamlit UI
250
+ # -----------------------------
251
+ st.set_page_config(page_title='Renewable Energy System Optimization with LMP', layout='wide')
252
+ st.title('Renewable Energy System Optimization + LMP (dual)')
253
 
254
  st.markdown("""
255
+ 最適化モデルに双対変数を用いた LMP 計算を追加しました。投資 LP における各時刻の限界コスト(長期的な影響)として解釈してください。
 
 
 
 
 
 
 
256
  """)
257
 
258
  with st.sidebar:
 
268
  offshore_wind_range = st.slider("Offshore Wind Capacity Range (MW)", 0, 10000, (0, 10000))
269
  river_range = st.slider("River Capacity Range (MW)", 0, 10000, (0, 10000))
270
 
271
+ if 'last_result' not in st.session_state:
272
+ st.session_state['last_result'] = None
273
 
274
+ col_a, col_b = st.columns(2)
275
+
276
+ with col_a:
277
+ if st.button('Calculate Optimal Energy Mix'):
278
+ res = optimize_with_duals(
279
+ solar_cost, onshore_wind_cost, offshore_wind_cost, river_cost,
280
+ battery_cost, yearly_demand, solar_range, wind_range, river_range, offshore_wind_range
281
+ )
282
+ st.session_state['last_result'] = res
283
+
284
+ st.success(f"Solve status: {res['status']} | Objective (¥): {res['objective']:.2f}")
285
+ fig_energy = make_supply_plot(res['df'], res['caps'], res['charge'], res['discharge'], res['demand'], res['curtail'])
286
+ st.plotly_chart(fig_energy, use_container_width=True, height=600)
287
+
288
+ with col_b:
289
+ if st.button('Compute LMP (dual)'):
290
+ res = st.session_state.get('last_result')
291
+ if not res:
292
+ st.error("Please run optimization first.")
293
+ else:
294
+ lmp_ser = res['lmp']
295
+ st.plotly_chart(make_lmp_fig(lmp_ser), use_container_width=True, height=600)
296
+ st.plotly_chart(make_lmp_hist(lmp_ser), use_container_width=True, height=400)
297
+
298
+ # Top-24 hours by LMP
299
+ top_df = lmp_ser.sort_values(ascending=False).head(24).reset_index()
300
+ top_df.columns = ['Time', 'LMP']
301
+ st.markdown("### Top-24 hours by LMP")
302
+ st.dataframe(top_df)
303
+
304
+ # Download
305
+ csv_buf = StringIO()
306
+ out_df = lmp_ser.reset_index()
307
+ out_df.to_csv(csv_buf, index=False, encoding='utf-8')
308
+ st.download_button("Download LMP CSV", data=csv_buf.getvalue(), file_name="lmp_timeseries.csv", mime="text/csv")
309
+
310
+ st.markdown("---")
311
+ st.markdown("ヒント:現実的な短期 LMP を得るには、技術ごとの可変費用(¥/MWh)、ネットワーク潮流・線容量制約の導入が有効です。")