Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
#
|
|
|
|
|
|
|
| 11 |
def get_json():
|
| 12 |
"""
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
-
with open('data.json') as f:
|
| 16 |
-
|
| 17 |
-
if not
|
| 18 |
-
return None
|
| 19 |
-
|
| 20 |
-
base_times =
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
for
|
| 24 |
-
if 'x' in
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
for
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
y=pivot_df.index,
|
| 156 |
-
aspect="auto",
|
| 157 |
-
color_continuous_scale='Plasma'
|
| 158 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 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 |
-
|
| 211 |
-
|
| 212 |
-
#
|
| 213 |
-
def
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
-
#
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
| 262 |
|
| 263 |
st.markdown("""
|
| 264 |
-
|
| 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 |
-
|
|
|
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 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)、ネットワーク潮流・線容量制約の導入が有効です。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|