# 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()]))