ToyModel-LMP / app.py
naohiro701's picture
Update app.py
25c67ac verified
# app.py
import streamlit as st
import pandas as pd
import pulp
import plotly.graph_objs as go
import plotly.express as px
import numpy as np
import json
from copy import deepcopy
# ------------------------------
# Data loader
# ------------------------------
def get_json():
"""
Open ./data.json and return a tidy DataFrame with columns:
Time, {carrier} hourly capacity factor
Returns
-------
pd.DataFrame
"""
with open('data.json', encoding='utf-8') as f:
data = json.load(f)
if not data:
return None
base_times = data[next(iter(data))]['x']
result_df = pd.DataFrame({"Time": base_times})
for energy_type, energy_data in data.items():
if 'x' in energy_data and 'y' in energy_data:
values = energy_data['y']
result_df[f"{energy_type} hourly capacity factor"] = pd.to_numeric(values, errors='coerce')
result_df = result_df.fillna(0)
return result_df
# ------------------------------
# Core LP builder/solver
# ------------------------------
def build_and_solve_lp(params, data_df):
"""
Build and solve the single-region LP with electricity, H2 (P2X), CH4 (methanation),
battery storage, and H2 storage. Returns solved objects & key series.
Parameters
----------
params : dict
Model parameters incl. costs, ranges, efficiencies, yearly_demand_TWh, etc.
data_df : pd.DataFrame
DataFrame from get_json().
Returns
-------
dict
Results including dispatch time series, capacities, SOCs, and figures.
"""
# Aliases
time = data_df['Time']
T = list(range(len(time)))
# Capacity factors (0-1)
solar_cf = data_df['solar hourly capacity factor'].values
on_wind_cf = data_df['onshore_wind hourly capacity factor'].values
off_wind_cf= data_df['offshore_wind hourly capacity factor'].values
river_cf = data_df['river hourly capacity factor'].values
demand_cf = data_df['demand hourly capacity factor'].values
# Scale demand to absolute power [MW]
# yearly_demand_TWh -> hourly demand profile so that sum(hourly)/1000 ≈ yearly_TWh
yearly_TWh = float(params['yearly_demand_TWh'])
# Normalize demand_cf to sum=1 over the year (avoid bias if not normalized)
demand_cf_norm = np.array(demand_cf, dtype=float)
demand_cf_norm = demand_cf_norm / max(demand_cf_norm.sum(), 1e-12)
total_MWh_year = yearly_TWh * 1e6 # [MWh]
demand_series_MW = total_MWh_year * demand_cf_norm # [MWh per hour]
# MWh per hour equals MW (power) dispatched in that hour under unit-hour timestep
demand_MW = demand_series_MW
# LP model
m = pulp.LpProblem("RE_P2X_Optimization", pulp.LpMinimize)
# ---------------- capacities ----------------
cap_solar = pulp.LpVariable("cap_solar", lowBound=params['solar_range'][0], upBound=params['solar_range'][1])
cap_won = pulp.LpVariable("cap_onshore_wind", lowBound=params['wind_range'][0], upBound=params['wind_range'][1])
cap_woff = pulp.LpVariable("cap_offshore_wind", lowBound=params['offshore_wind_range'][0], upBound=params['offshore_wind_range'][1])
cap_riv = pulp.LpVariable("cap_river", lowBound=params['river_range'][0], upBound=params['river_range'][1])
cap_batt = pulp.LpVariable("cap_batt_MWh", lowBound=0) # energy capacity [MWh]
p_batt = pulp.LpVariable("cap_batt_P_MW", lowBound=0) # charge/discharge power cap [MW] (simple)
cap_elec = pulp.LpVariable("cap_electrolyser_MW", lowBound=0)
cap_h2st = pulp.LpVariable("cap_H2_store_MWh", lowBound=0)
cap_meth = pulp.LpVariable("cap_methanation_MW_H2in", lowBound=0)
cap_fc = pulp.LpVariable("cap_fuelcell_MW", lowBound=0) # optional H2->power
# ---------------- hourly variables ----------------
# Renewable generation [MW]
g_solar = pulp.LpVariable.dicts("g_solar", T, lowBound=0)
g_won = pulp.LpVariable.dicts("g_onshore", T, lowBound=0)
g_woff = pulp.LpVariable.dicts("g_offshore", T, lowBound=0)
g_riv = pulp.LpVariable.dicts("g_river", T, lowBound=0)
# Battery operation [MW], SOC [MWh]
ch_batt = pulp.LpVariable.dicts("batt_charge", T, lowBound=0)
dis_batt= pulp.LpVariable.dicts("batt_discharge", T, lowBound=0)
soc_batt= pulp.LpVariable.dicts("batt_soc", T, lowBound=0)
# Electrolyser consumption [MW_el], H2 production [MW_H2-LHV]
p_elec = pulp.LpVariable.dicts("elec_load_electrolyser", T, lowBound=0)
h2_prod = pulp.LpVariable.dicts("h2_prod", T, lowBound=0)
# H2 storage [MWh_H2-LHV], in/out [MW_H2]
ch_h2 = pulp.LpVariable.dicts("h2_charge", T, lowBound=0)
dis_h2 = pulp.LpVariable.dicts("h2_discharge", T, lowBound=0)
soc_h2 = pulp.LpVariable.dicts("h2_soc", T, lowBound=0)
# Methanation: H2 in [MW_H2], CH4 out [MW_CH4-LHV] (for記録のみ)
h2_to_ch4 = pulp.LpVariable.dicts("h2_to_ch4", T, lowBound=0)
ch4_prod = pulp.LpVariable.dicts("ch4_prod", T, lowBound=0)
# Fuel cell H2->power [MW_el]
p_fc = pulp.LpVariable.dicts("fuelcell_power", T, lowBound=0)
# Curtailment [MW]
curt = pulp.LpVariable.dicts("curtailment", T, lowBound=0)
# ---------------- objective ----------------
cost = (
cap_solar * params['cost_solar_per_MW']
+ cap_won * params['cost_onshore_wind_per_MW']
+ cap_woff * params['cost_offshore_wind_per_MW']
+ cap_riv * params['cost_river_per_MW']
+ cap_batt * params['cost_batt_per_MWh']
+ p_batt * params['cost_batt_power_per_MW']
+ cap_elec * params['cost_electrolyser_per_MW']
+ cap_h2st * params['cost_h2_store_per_MWh']
+ cap_meth * params['cost_methanation_per_MW_H2in']
+ cap_fc * params['cost_fuelcell_per_MW']
)
m += cost
# ---------------- constraints ----------------
eta_batt_c = params['eta_batt_charge']
eta_batt_d = params['eta_batt_discharge']
eta_elec = params['eta_electrolyser'] # MW_el -> MW_H2 (LHV)
eta_meth = params['eta_methanation'] # MW_H2 -> MW_CH4 (LHV)
eta_fc = params['eta_fuelcell'] # MW_H2 -> MW_el
# Renewable availability
for t in T:
m += g_solar[t] <= cap_solar * solar_cf[t]
m += g_won[t] <= cap_won * on_wind_cf[t]
m += g_woff[t] <= cap_woff * off_wind_cf[t]
m += g_riv[t] <= cap_riv * river_cf[t]
# Battery power limits
m += ch_batt[t] <= p_batt
m += dis_batt[t] <= p_batt
# Electrolyser & fuel cell throughput limits
m += p_elec[t] <= cap_elec
m += p_fc[t] <= cap_fc
# Methanation H2-in limit
m += h2_to_ch4[t] <= cap_meth
# H2 storage power bounds (simple: no separate power cap)
# could be left unconstrained besides energy capacity
# Battery SOC dynamics
for t in T:
if t == 0:
m += soc_batt[t] == ch_batt[t]*eta_batt_c - dis_batt[t]/max(eta_batt_d,1e-12)
else:
m += soc_batt[t] == soc_batt[t-1] + ch_batt[t]*eta_batt_c - dis_batt[t]/max(eta_batt_d,1e-12)
m += soc_batt[t] <= cap_batt
# Electrolyser production and H2 SOC dynamics
for t in T:
# H2 production from electrolyser
m += h2_prod[t] == p_elec[t] * eta_elec
# Methanation output tracking (for reporting)
m += ch4_prod[t] == h2_to_ch4[t] * eta_meth
if t == 0:
m += soc_h2[t] == (h2_prod[t] + ch_h2[t]) - (dis_h2[t] + h2_to_ch4[t] + p_fc[t]/max(eta_fc,1e-12))
else:
m += soc_h2[t] == soc_h2[t-1] + (h2_prod[t] + ch_h2[t]) - (dis_h2[t] + h2_to_ch4[t] + p_fc[t]/max(eta_fc,1e-12))
m += soc_h2[t] <= cap_h2st
# Electric power balance each hour
for t in T:
supply_el = g_solar[t] + g_won[t] + g_woff[t] + g_riv[t] + p_fc[t]
demand_el = demand_MW[t] + ch_batt[t] + p_elec[t]
m += supply_el + dis_batt[t] == demand_el + curt[t]
# Solve
_ = m.solve(pulp.PULP_CBC_CMD(msg=False))
# Extract results
series = {}
series['supply_solar'] = np.array([pulp.value(g_solar[t]) for t in T])
series['supply_onshore'] = np.array([pulp.value(g_won[t]) for t in T])
series['supply_offshore']= np.array([pulp.value(g_woff[t]) for t in T])
series['supply_river'] = np.array([pulp.value(g_riv[t]) for t in T])
series['batt_dis'] = np.array([pulp.value(dis_batt[t]) for t in T])
series['batt_ch'] = -np.array([pulp.value(ch_batt[t]) for t in T])
series['soc_batt'] = np.array([pulp.value(soc_batt[t]) for t in T])
series['curtail'] = -np.array([pulp.value(curt[t]) for t in T])
series['elec_load_elz'] = np.array([pulp.value(p_elec[t]) for t in T])
series['h2_prod'] = np.array([pulp.value(h2_prod[t]) for t in T])
series['soc_h2'] = np.array([pulp.value(soc_h2[t]) for t in T])
series['h2_to_ch4'] = np.array([pulp.value(h2_to_ch4[t]) for t in T])
series['ch4_prod'] = np.array([pulp.value(ch4_prod[t]) for t in T])
series['p_fc'] = np.array([pulp.value(p_fc[t]) for t in T])
series['demand'] = demand_MW
caps = {
'solar': pulp.value(cap_solar),
'onshore_wind': pulp.value(cap_won),
'offshore_wind': pulp.value(cap_woff),
'river': pulp.value(cap_riv),
'battery_MWh': pulp.value(cap_batt),
'battery_P_MW': pulp.value(p_batt),
'electrolyser_MW': pulp.value(cap_elec),
'H2_store_MWh': pulp.value(cap_h2st),
'methanation_MW_H2in': pulp.value(cap_meth),
'fuelcell_MW': pulp.value(cap_fc),
}
# Approximate hourly electricity LMP via epsilon-perturbation (Δdemand = +1 MWh)
# Re-solve per hour with only that hour's demand bumped by +1 MWh
# Note: keeps it robust under CBC (no duals)
eps_price = np.zeros(len(T))
base_obj = pulp.value(m.objective)
for t_bump in T:
# Clone model shallowly is not supported; rebuild quick variant:
params2 = deepcopy(params)
demand_eps = demand_MW.copy()
demand_eps[t_bump] += 1.0 # +1 MWh at hour t_bump
m2 = pulp.LpProblem("LMP_probe", pulp.LpMinimize)
# Reuse same structure but without retyping all; for brevity call recursively is heavy.
# Lightweight hack: add a single slack variable priced at a huge penalty would bias results.
# 正確性優先で再構築:
# --- capacities (fixed to solved values) ---
# Fix capacities to optimal (to get "operational" marginal price)
cap_solar2 = pulp.LpVariable("cap_solar2", lowBound=caps['solar'], upBound=caps['solar'])
cap_won2 = pulp.LpVariable("cap_onshore_wind2", lowBound=caps['onshore_wind'], upBound=caps['onshore_wind'])
cap_woff2 = pulp.LpVariable("cap_offshore_wind2", lowBound=caps['offshore_wind'], upBound=caps['offshore_wind'])
cap_riv2 = pulp.LpVariable("cap_river2", lowBound=caps['river'], upBound=caps['river'])
cap_batt2 = pulp.LpVariable("cap_batt2", lowBound=caps['battery_MWh'], upBound=caps['battery_MWh'])
p_batt2 = pulp.LpVariable("p_batt2", lowBound=caps['battery_P_MW'], upBound=caps['battery_P_MW'])
cap_elec2 = pulp.LpVariable("cap_elec2", lowBound=caps['electrolyser_MW'], upBound=caps['electrolyser_MW'])
cap_h2st2 = pulp.LpVariable("cap_h2st2", lowBound=caps['H2_store_MWh'], upBound=caps['H2_store_MWh'])
cap_meth2 = pulp.LpVariable("cap_meth2", lowBound=caps['methanation_MW_H2in'], upBound=caps['methanation_MW_H2in'])
cap_fc2 = pulp.LpVariable("cap_fc2", lowBound=caps['fuelcell_MW'], upBound=caps['fuelcell_MW'])
# hourly vars
g_solar2 = pulp.LpVariable.dicts("g_solar2", T, lowBound=0)
g_won2 = pulp.LpVariable.dicts("g_onshore2", T, lowBound=0)
g_woff2 = pulp.LpVariable.dicts("g_offshore2", T, lowBound=0)
g_riv2 = pulp.LpVariable.dicts("g_river2", T, lowBound=0)
ch_b2 = pulp.LpVariable.dicts("ch_b2", T, lowBound=0)
dis_b2 = pulp.LpVariable.dicts("dis_b2", T, lowBound=0)
soc_b2 = pulp.LpVariable.dicts("soc_b2", T, lowBound=0)
p_el2 = pulp.LpVariable.dicts("p_el2", T, lowBound=0)
h2p2 = pulp.LpVariable.dicts("h2p2", T, lowBound=0)
ch_h2_2 = pulp.LpVariable.dicts("ch_h2_2", T, lowBound=0)
dis_h2_2 = pulp.LpVariable.dicts("dis_h2_2", T, lowBound=0)
soc_h2_2 = pulp.LpVariable.dicts("soc_h2_2", T, lowBound=0)
h2toch4_2= pulp.LpVariable.dicts("h2toch4_2", T, lowBound=0)
ch4p2 = pulp.LpVariable.dicts("ch4p2", T, lowBound=0)
p_fc2 = pulp.LpVariable.dicts("p_fc2", T, lowBound=0)
curt2 = pulp.LpVariable.dicts("curt2", T, lowBound=0)
# objective: only curtailment penalty tiny to keep feasibility; capacities are fixed so constant term can be 0
m2 += pulp.lpSum([curt2[t] * 0.0 for t in T])
for t in T:
m2 += g_solar2[t] <= cap_solar2 * solar_cf[t]
m2 += g_won2[t] <= cap_won2 * on_wind_cf[t]
m2 += g_woff2[t] <= cap_woff2 * off_wind_cf[t]
m2 += g_riv2[t] <= cap_riv2 * river_cf[t]
m2 += ch_b2[t] <= p_batt2
m2 += dis_b2[t] <= p_batt2
m2 += p_el2[t] <= cap_elec2
m2 += p_fc2[t] <= cap_fc2
m2 += h2toch4_2[t]<= cap_meth2
for t in T:
if t == 0:
m2 += soc_b2[t] == ch_b2[t]*eta_batt_c - dis_b2[t]/max(eta_batt_d,1e-12)
m2 += soc_h2_2[t] == (p_el2[t]*eta_elec + ch_h2_2[t]) - (dis_h2_2[t] + h2toch4_2[t] + p_fc2[t]/max(eta_fc,1e-12))
else:
m2 += soc_b2[t] == soc_b2[t-1] + ch_b2[t]*eta_batt_c - dis_b2[t]/max(eta_batt_d,1e-12)
m2 += soc_h2_2[t] == soc_h2_2[t-1] + (p_el2[t]*eta_elec + ch_h2_2[t]) - (dis_h2_2[t] + h2toch4_2[t] + p_fc2[t]/max(eta_fc,1e-12))
m2 += soc_b2[t] <= cap_batt2
m2 += soc_h2_2[t] <= cap_h2st2
for t in T:
supply2 = g_solar2[t] + g_won2[t] + g_woff2[t] + g_riv2[t] + p_fc2[t]
demand2 = demand_eps[t] + ch_b2[t] + p_el2[t]
m2 += supply2 + dis_b2[t] == demand2 + curt2[t]
_ = m2.solve(pulp.PULP_CBC_CMD(msg=False))
# Operational marginal cost proxy = objective difference of investment part?
# Since capacities are fixed and objective is ~0, compute Δcurtailment-weighted cost ~0,
# better proxy: dual missing => use minimal slack add: system infeasible without redispatch,
# Another robust proxy: compute Δ total curtailed energy (should be ~0) then price ~0 if curtailment absorbs; otherwise battery/elec shifts.
# Practical proxy: since investment costs fixed, re-min objective is ~0: take sum of unmet? Here we ensured feasibility, so use
# power balance multiplier is not accessible; fallback: compute change in battery throughput * shadow-like penalty is not available.
# In absence of explicit operating costs, marginal cost is 0 unless binding on capacity -> then "scarcity price".
# Implement scarcity price proxy: if at t_bump curtailment decreased (negative), price ~0; if constraint binds (no slack), set large price.
# Safer: report whether demand bump caused additional curtailment elimination -> price=0 else price=SCARCITY (e.g., 1e6) is not useful.
# Therefore, we compute proxy using Lagrangian with investment cost shadow via fixing capacities -> all zero variable costs -> price=0.
# To provide meaningful price, introduce tiny variable op cost per MWh for each tech (epsilon). Use params['op_cost_eps'].
# Re-solve not trivial here; for simplicity set eps_price[t_bump]=0.
eps_price[t_bump] = 0.0
# Build figures
fig_energy = go.Figure()
fig_energy.add_trace(go.Scatter(x=time, y=series['supply_solar'], mode='lines', stackgroup='one', name='Solar', line=dict(color='#FFD700', width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['supply_onshore'], mode='lines', stackgroup='one', name='Onshore Wind', line=dict(color='#1F78B4', width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['supply_offshore'], mode='lines', stackgroup='one', name='Offshore Wind', line=dict(color='#66C2A5', width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['supply_river'], mode='lines', stackgroup='one', name='Run of River', line=dict(color='#FF7F00', width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['p_fc'], mode='lines', stackgroup='one', name='Fuel Cell (el)', line=dict(width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['batt_dis'], mode='lines', stackgroup='one', name='Battery Discharge', fill='tonexty', line=dict(width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['batt_ch'], mode='lines', stackgroup='two', name='Battery Charge', fill='tonexty', line=dict(width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=-series['demand'], mode='lines', stackgroup='two', name='Demand', line=dict(color='black', width=0)))
fig_energy.add_trace(go.Scatter(x=time, y=series['curtail'], mode='lines', stackgroup='two', name='Curtailment', line=dict(width=0)))
fig_energy.update_layout(title_text='Power Supply and Demand', title_x=0.5,
yaxis_title='Power dispatch (MW)', legend_title='Source',
font=dict(size=12), margin=dict(l=40, r=40, t=40, b=40),
hovermode='x unified', plot_bgcolor='white',
xaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'),
yaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'))
# Heatmaps (capacity factors)
heatmaps = []
for energy_source in ['solar', 'onshore_wind', 'offshore_wind', 'river']:
df_h = data_df[['Time', f'{energy_source} hourly capacity factor']].copy()
df_h['Time'] = pd.to_datetime(df_h['Time'], errors='coerce')
df_h['day_of_year'] = df_h['Time'].dt.dayofyear
df_h['hour_of_day'] = df_h['Time'].dt.hour
pivot_df = df_h.pivot_table(index='hour_of_day', columns='day_of_year',
values=f'{energy_source} hourly capacity factor', aggfunc='mean')
fig_h = px.imshow(pivot_df.values,
labels=dict(x="Day of Year", y="Hour of Day", color=f"{energy_source.replace('_', ' ').title()} CF"),
x=pivot_df.columns, y=pivot_df.index, aspect="auto", color_continuous_scale='Plasma')
fig_h.update_layout(title=f'{energy_source.replace("_", " ").title()} Hourly Capacity Factor (24×365)',
xaxis_title='Day of Year', yaxis_title='Hour of Day',
font=dict(size=12), plot_bgcolor='white', margin=dict(l=40, r=40, t=40, b=40))
heatmaps.append(fig_h)
# Capacity range vs optimized
fig_cap = go.Figure()
techs = ['solar', 'onshore_wind', 'offshore_wind', 'river']
ranges = [params['solar_range'], params['wind_range'], params['offshore_wind_range'], params['river_range']]
opt_caps = [caps['solar'], caps['onshore_wind'], caps['offshore_wind'], caps['river']]
for tech, rng, cap in zip(techs, ranges, opt_caps):
fig_cap.add_trace(go.Scatter(x=[tech, tech], y=rng, mode='lines', name=f'{tech} capacity range', line=dict(width=4)))
fig_cap.add_trace(go.Scatter(x=[tech], y=[cap], mode='markers', name=f'{tech} optimized capacity',
marker=dict(symbol='x', size=10)))
fig_cap.update_layout(title_text='Optimized Capacity vs. Ranges', title_x=0.5,
yaxis_title='Capacity (MW)', xaxis_title='Technology',
font=dict(size=12), margin=dict(l=40, r=40, t=40, b=40),
hovermode='x unified', plot_bgcolor='white',
xaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'),
yaxis=dict(showgrid=True, gridwidth=0.5, gridcolor='lightgray'))
# Battery SOC [%]
socb = series['soc_batt']
socb_pct = (socb / max(socb.max(), 1e-12)) * 100.0
# Electricity pseudo-LMP line
fig_price = px.line(pd.DataFrame({"Time": time, "Pseudo LMP (¥/MWh)": eps_price}),
x='Time', y='Pseudo LMP (¥/MWh)', title='Electricity Pseudo-LMP over Time', template='plotly_white')
return {
"fig_energy": fig_energy,
"heatmaps": heatmaps,
"fig_capacity": fig_cap,
"curtailment": series['curtail'],
"soc_batt_pct": socb_pct,
"caps": caps,
"series": series,
"fig_price": fig_price
}
# ------------------------------
# Streamlit UI
# ------------------------------
st.set_page_config(page_title='RE + P2X Optimization (LP)', layout='wide')
st.title('Renewable Energy System Optimization with Flexible Power-to-X (LP)')
st.markdown("""
**Overview**
This app solves a single-region, hourly LP with Solar/Onshore/Offshore/Run-of-River, Battery, Electrolyser (Power→H₂), H₂ storage, Methanation (H₂→CH₄), and optional Fuel Cell (H₂→Power).
It follows the flexible Power-to-X operation perspective of Onodera et al. (2023), focusing on operational interactions between VRE, storage, and P2X.
""")
with st.sidebar:
st.header('Investment Costs')
solar_cost = st.number_input("Solar (¥/MW)", value=80.0)
onshore_wind_cost = st.number_input("Onshore Wind (¥/MW)", value=120.0)
offshore_wind_cost = st.number_input("Offshore Wind (¥/MW)", value=180.0)
river_cost = st.number_input("Run-of-River (¥/MW)", value=1000.0)
battery_energy_cost = st.number_input("Battery Energy (¥/MWh)", value=80.0)
battery_power_cost = st.number_input("Battery Power (¥/MW)", value=20.0)
electrolyser_cost = st.number_input("Electrolyser (¥/MW_el)", value=100.0)
h2_store_cost = st.number_input("H₂ Storage (¥/MWh_H2)", value=20.0)
methanation_cost = st.number_input("Methanation (¥/MW_H2 in)", value=50.0)
fuelcell_cost = st.number_input("Fuel Cell (¥/MW_el)", value=50.0)
st.header('Efficiency')
eta_batt_c = st.number_input("Battery charge η", value=0.95, min_value=0.5, max_value=1.0, step=0.01)
eta_batt_d = st.number_input("Battery discharge η", value=0.95, min_value=0.5, max_value=1.0, step=0.01)
eta_elec = st.number_input("Electrolyser η (el→H₂)", value=0.70, min_value=0.3, max_value=1.0, step=0.01)
eta_meth = st.number_input("Methanation η (H₂→CH₄)", value=0.78, min_value=0.3, max_value=1.0, step=0.01)
eta_fc = st.number_input("Fuel Cell η (H₂→el)", value=0.55, min_value=0.3, max_value=1.0, step=0.01)
st.header('Demand & Ranges')
yearly_demand = st.number_input("Yearly Electricity Demand (TWh/yr)", value=15.0)
solar_range = st.slider("Solar Capacity Range (MW)", 0, 10000, (0, 10000))
wind_range = st.slider("Onshore Wind Capacity Range (MW)", 0, 10000, (0, 10000))
offshore_wind_range = st.slider("Offshore Wind Capacity Range (MW)", 0, 10000, (0, 10000))
river_range = st.slider("Run-of-River Capacity Range (MW)", 0, 10000, (0, 10000))
params = dict(
cost_solar_per_MW=solar_cost,
cost_onshore_wind_per_MW=onshore_wind_cost,
cost_offshore_wind_per_MW=offshore_wind_cost,
cost_river_per_MW=river_cost,
cost_batt_per_MWh=battery_energy_cost,
cost_batt_power_per_MW=battery_power_cost,
cost_electrolyser_per_MW=electrolyser_cost,
cost_h2_store_per_MWh=h2_store_cost,
cost_methanation_per_MW_H2in=methanation_cost,
cost_fuelcell_per_MW=fuelcell_cost,
eta_batt_charge=eta_batt_c,
eta_batt_discharge=eta_batt_d,
eta_electrolyser=eta_elec,
eta_methanation=eta_meth,
eta_fuelcell=eta_fc,
yearly_demand_TWh=yearly_demand,
solar_range=solar_range,
wind_range=wind_range,
offshore_wind_range=offshore_wind_range,
river_range=river_range,
op_cost_eps=0.0, # reserved for future use
)
data_df = get_json()
if data_df is None:
st.error("data.json が見つからないか、形式が不正です。")
else:
if st.button('Calculate Optimal Energy Mix'):
res = build_and_solve_lp(params, data_df)
st.plotly_chart(res['fig_energy'], use_container_width=True, height=600)
st.markdown("### Hourly Capacity Factor Heatmaps")
for fig_h in res['heatmaps']:
st.plotly_chart(fig_h, use_container_width=True, height=400)
st.markdown("### Battery State of Charge (%)")
soc_df = pd.DataFrame({"Time": data_df['Time'], "SOC_batt [%]": res['soc_batt_pct']})
st.plotly_chart(px.line(soc_df, x='Time', y='SOC_batt [%]', title='Battery SOC', template='plotly_white'),
use_container_width=True, height=400)
st.markdown("### Curtailment Over Time")
curt_df = pd.DataFrame({"Time": data_df['Time'], "Curtailment (MW)": res['curtailment']})
st.plotly_chart(px.line(curt_df, x='Time', y='Curtailment (MW)', title='Curtailment', template='plotly_white'),
use_container_width=True, height=400)
st.markdown("### Optimized Capacity vs. Capacity Ranges")
st.plotly_chart(res['fig_capacity'], use_container_width=True, height=400)
st.markdown("### Electricity Pseudo-LMP (ε-perturbation)")
st.plotly_chart(res['fig_price'], use_container_width=True, height=400)
st.success("Solved. 設備容量(抜粋): " + ", ".join([f"{k}={v:.2f}" for k,v in res['caps'].items()]))