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