import streamlit as st import requests import pandas as pd import pulp import plotly.graph_objs as go import plotly.express as px import numpy as np import matplotlib.pyplot as plt # Renewable energy data fetch function def get_renewable_energy_data(city_code): url = f"https://energy-sustainability.jp/_ajax/renewable_energy/get/?code={city_code}" response = requests.get(url) if response.status_code != 200: return None, "Failed to retrieve data." data = response.json() if not data: return None, "No data found." 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"] = values return result_df, None # Optimize energy system and use MGA def optimize_energy_system(city_code, solar_cost, onshore_wind_cost, offshore_wind_cost, river_cost, battery_cost, yearly_demand, solar_range, wind_range, river_range, offshore_wind_range, nuclear_range, thermal_range, nuclear_cf, thresholds, selected_tech, nuclear_cost_capex, nuclear_cost_opex, thermal_cost_capex, thermal_cost_opex): data, error = get_renewable_energy_data(city_code) if error: st.error(error) return None, None, None, None, None, None, None for col in data.columns[1:]: data[col] = pd.to_numeric(data[col], errors='coerce') data = data.fillna(0) time_steps = range(len(data['Time'])) solar_cf = data['solar hourly capacity factor'] onshore_wind_cf = data['onshore_wind hourly capacity factor'] offshore_wind_cf = data['offshore_wind hourly capacity factor'] river_cf = data['river hourly capacity factor'] demand_cf = data['demand hourly capacity factor'] regions = ['region1'] technologies = ['solar', 'onshore_wind', 'offshore_wind', 'river', 'nuclear', 'thermal'] capacity_factor = { 'solar': solar_cf, 'onshore_wind': onshore_wind_cf, 'offshore_wind': offshore_wind_cf, 'river': river_cf, 'nuclear': [nuclear_cf] * len(time_steps), 'thermal': [0] * len(time_steps) # Thermal capacity factor is not used } capacity_cost = { 'solar': solar_cost, 'onshore_wind': onshore_wind_cost, 'offshore_wind': offshore_wind_cost, 'river': river_cost, 'nuclear': nuclear_cost_capex, 'thermal': thermal_cost_capex } opex_cost = { 'solar': 0, 'onshore_wind': 0, 'offshore_wind': 0, 'river': 0, 'nuclear': nuclear_cost_opex, 'thermal': thermal_cost_opex } battery_cost_per_mwh = battery_cost battery_efficiency = 0.9 demand = demand_cf * yearly_demand / 100 * 1000 * 1000 # Convert TWh to MWh capacity = pulp.LpVariable.dicts("capacity", [(r, g) for r in regions for g in technologies], lowBound=0, cat='Continuous') curtailment = pulp.LpVariable.dicts("curtailment", [(r, t) for r in regions for t in time_steps], lowBound=0, cat='Continuous') battery_capacity = pulp.LpVariable("battery_capacity", lowBound=0, cat='Continuous') battery_charge = pulp.LpVariable.dicts("battery_charge", time_steps, lowBound=0, cat='Continuous') battery_discharge = pulp.LpVariable.dicts("battery_discharge", time_steps, lowBound=0, cat='Continuous') SOC = pulp.LpVariable.dicts("SOC", time_steps, lowBound=0, cat='Continuous') thermal_generation = pulp.LpVariable.dicts("thermal_generation", time_steps, lowBound=0, cat='Continuous') nuclear_generation = pulp.LpVariable.dicts("nuclear_generation", time_steps, lowBound=0, cat='Continuous') model = pulp.LpProblem("EnergySystemOptimizationWithBattery", pulp.LpMinimize) # Objective: minimize total cost (capacities, battery, OPEX) total_opex = pulp.lpSum([thermal_generation[t] * opex_cost['thermal'] + nuclear_generation[t] * opex_cost['nuclear'] for t in time_steps]) model += pulp.lpSum([capacity[(r, g)] * capacity_cost[g] for r in regions for g in technologies]) + \ battery_capacity * battery_cost_per_mwh + total_opex, "TotalCost" # Constraints: meet demand, manage battery SOC for r in regions: for t in time_steps: renewable_generation = pulp.lpSum([capacity[(r, g)] * capacity_factor[g][t] for g in ['solar', 'onshore_wind', 'offshore_wind', 'river']]) model += renewable_generation + thermal_generation[t] + nuclear_generation[t] + battery_discharge[t] == demand[t] + battery_charge[t] + curtailment[(r, t)], f"DemandConstraint_{r}_{t}" if t == 0: model += SOC[t] == battery_charge[t] * battery_efficiency - battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" else: model += SOC[t] == SOC[t - 1] + battery_charge[t] * battery_efficiency - battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" model += SOC[t] <= battery_capacity, f"SOCUpperBound_{t}" # Thermal generation cannot exceed capacity model += thermal_generation[t] <= capacity[(r, 'thermal')], f"ThermalCapacityConstraint_{t}" # Nuclear generation is fixed at capacity * capacity factor model += nuclear_generation[t] == capacity[(r, 'nuclear')] * nuclear_cf, f"NuclearGenerationConstraint_{t}" # Capacity range constraints model += capacity[('region1', 'solar')] >= solar_range[0] model += capacity[('region1', 'solar')] <= solar_range[1] model += capacity[('region1', 'onshore_wind')] >= wind_range[0] model += capacity[('region1', 'onshore_wind')] <= wind_range[1] model += capacity[('region1', 'offshore_wind')] >= offshore_wind_range[0] model += capacity[('region1', 'offshore_wind')] <= offshore_wind_range[1] model += capacity[('region1', 'river')] >= river_range[0] model += capacity[('region1', 'river')] <= river_range[1] model += capacity[('region1', 'nuclear')] >= nuclear_range[0] model += capacity[('region1', 'nuclear')] <= nuclear_range[1] model += capacity[('region1', 'thermal')] >= thermal_range[0] model += capacity[('region1', 'thermal')] <= thermal_range[1] # Solve the initial model to find the optimal solution model.solve() optimal_cost = pulp.value(model.objective) # Store initial total OPEX total_opex_value = sum(thermal_generation[t].varValue * opex_cost['thermal'] + nuclear_generation[t].varValue * opex_cost['nuclear'] for t in time_steps) # MGA: Generate alternative solutions for selected technologies only alternative_solutions = [] for threshold in thresholds: relaxed_cost = optimal_cost * (1 + threshold) for tech in selected_tech: # Minimize capacity of each technology alt_model_min = pulp.LpProblem(f"AlternativeModel_Min_{tech}_{threshold}", pulp.LpMinimize) # Define new variables for the alternative model alt_capacity = pulp.LpVariable.dicts("capacity", [(r, g) for r in regions for g in technologies], lowBound=0, cat='Continuous') alt_thermal_generation = pulp.LpVariable.dicts("thermal_generation", time_steps, lowBound=0, cat='Continuous') alt_nuclear_generation = pulp.LpVariable.dicts("nuclear_generation", time_steps, lowBound=0, cat='Continuous') alt_curtailment = pulp.LpVariable.dicts("curtailment", [(r, t) for r in regions for t in time_steps], lowBound=0, cat='Continuous') alt_battery_capacity = pulp.LpVariable("battery_capacity", lowBound=0, cat='Continuous') alt_battery_charge = pulp.LpVariable.dicts("battery_charge", time_steps, lowBound=0, cat='Continuous') alt_battery_discharge = pulp.LpVariable.dicts("battery_discharge", time_steps, lowBound=0, cat='Continuous') alt_SOC = pulp.LpVariable.dicts("SOC", time_steps, lowBound=0, cat='Continuous') # Total OPEX for alternative model alt_total_opex = pulp.lpSum([alt_thermal_generation[t] * opex_cost['thermal'] + alt_nuclear_generation[t] * opex_cost['nuclear'] for t in time_steps]) # Objective function alt_model_min += pulp.lpSum([alt_capacity[(r, g)] * capacity_cost[g] for r in regions for g in technologies]) + \ alt_battery_capacity * battery_cost_per_mwh + alt_total_opex, "TotalCost" # Cost constraint alt_model_min += pulp.lpSum([alt_capacity[(r, g)] * capacity_cost[g] for r in regions for g in technologies]) + \ alt_battery_capacity * battery_cost_per_mwh + alt_total_opex <= relaxed_cost # Constraints for r in regions: for t in time_steps: renewable_generation = pulp.lpSum([alt_capacity[(r, g)] * capacity_factor[g][t] for g in ['solar', 'onshore_wind', 'offshore_wind', 'river']]) alt_model_min += renewable_generation + alt_thermal_generation[t] + alt_nuclear_generation[t] + alt_battery_discharge[t] == demand[t] + alt_battery_charge[t] + alt_curtailment[(r, t)], f"DemandConstraint_{r}_{t}" if t == 0: alt_model_min += alt_SOC[t] == alt_battery_charge[t] * battery_efficiency - alt_battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" else: alt_model_min += alt_SOC[t] == alt_SOC[t - 1] + alt_battery_charge[t] * battery_efficiency - alt_battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" alt_model_min += alt_SOC[t] <= alt_battery_capacity, f"SOCUpperBound_{t}" # Thermal generation cannot exceed capacity alt_model_min += alt_thermal_generation[t] <= alt_capacity[(r, 'thermal')], f"ThermalCapacityConstraint_{t}" # Nuclear generation is fixed at capacity * capacity factor alt_model_min += alt_nuclear_generation[t] == alt_capacity[(r, 'nuclear')] * nuclear_cf, f"NuclearGenerationConstraint_{t}" # Capacity range constraints alt_model_min += alt_capacity[('region1', 'solar')] >= solar_range[0] alt_model_min += alt_capacity[('region1', 'solar')] <= solar_range[1] alt_model_min += alt_capacity[('region1', 'onshore_wind')] >= wind_range[0] alt_model_min += alt_capacity[('region1', 'onshore_wind')] <= wind_range[1] alt_model_min += alt_capacity[('region1', 'offshore_wind')] >= offshore_wind_range[0] alt_model_min += alt_capacity[('region1', 'offshore_wind')] <= offshore_wind_range[1] alt_model_min += alt_capacity[('region1', 'river')] >= river_range[0] alt_model_min += alt_capacity[('region1', 'river')] <= river_range[1] alt_model_min += alt_capacity[('region1', 'nuclear')] >= nuclear_range[0] alt_model_min += alt_capacity[('region1', 'nuclear')] <= nuclear_range[1] alt_model_min += alt_capacity[('region1', 'thermal')] >= thermal_range[0] alt_model_min += alt_capacity[('region1', 'thermal')] <= thermal_range[1] # Minimize the capacity of the selected technology alt_model_min += alt_capacity[('region1', tech)], f"Minimize_{tech}_Capacity" # Solve the alternative model alt_model_min.solve() if pulp.LpStatus[alt_model_min.status] == 'Optimal': solution = {g: alt_capacity[('region1', g)].varValue for g in technologies} solution['battery_capacity'] = alt_battery_capacity.varValue # Calculate total OPEX for the solution total_opex_value = sum(alt_thermal_generation[t].varValue * opex_cost['thermal'] + alt_nuclear_generation[t].varValue * opex_cost['nuclear'] for t in time_steps) alternative_solutions.append({ 'threshold': threshold, 'type': 'min', 'technology': tech, 'solution': solution, 'total_cost': pulp.value(alt_model_min.objective), 'total_opex': total_opex_value }) # Maximize capacity of each technology alt_model_max = pulp.LpProblem(f"AlternativeModel_Max_{tech}_{threshold}", pulp.LpMinimize) # Define new variables for the alternative model alt_capacity = pulp.LpVariable.dicts("capacity", [(r, g) for r in regions for g in technologies], lowBound=0, cat='Continuous') alt_thermal_generation = pulp.LpVariable.dicts("thermal_generation", time_steps, lowBound=0, cat='Continuous') alt_nuclear_generation = pulp.LpVariable.dicts("nuclear_generation", time_steps, lowBound=0, cat='Continuous') alt_curtailment = pulp.LpVariable.dicts("curtailment", [(r, t) for r in regions for t in time_steps], lowBound=0, cat='Continuous') alt_battery_capacity = pulp.LpVariable("battery_capacity", lowBound=0, cat='Continuous') alt_battery_charge = pulp.LpVariable.dicts("battery_charge", time_steps, lowBound=0, cat='Continuous') alt_battery_discharge = pulp.LpVariable.dicts("battery_discharge", time_steps, lowBound=0, cat='Continuous') alt_SOC = pulp.LpVariable.dicts("SOC", time_steps, lowBound=0, cat='Continuous') # Total OPEX for alternative model alt_total_opex = pulp.lpSum([alt_thermal_generation[t] * opex_cost['thermal'] + alt_nuclear_generation[t] * opex_cost['nuclear'] for t in time_steps]) # Objective function alt_model_max += pulp.lpSum([alt_capacity[(r, g)] * capacity_cost[g] for r in regions for g in technologies]) + \ alt_battery_capacity * battery_cost_per_mwh + alt_total_opex, "TotalCost" # Cost constraint alt_model_max += pulp.lpSum([alt_capacity[(r, g)] * capacity_cost[g] for r in regions for g in technologies]) + \ alt_battery_capacity * battery_cost_per_mwh + alt_total_opex <= relaxed_cost # Constraints for r in regions: for t in time_steps: renewable_generation = pulp.lpSum([alt_capacity[(r, g)] * capacity_factor[g][t] for g in ['solar', 'onshore_wind', 'offshore_wind', 'river']]) alt_model_max += renewable_generation + alt_thermal_generation[t] + alt_nuclear_generation[t] + alt_battery_discharge[t] == demand[t] + alt_battery_charge[t] + alt_curtailment[(r, t)], f"DemandConstraint_{r}_{t}" if t == 0: alt_model_max += alt_SOC[t] == alt_battery_charge[t] * battery_efficiency - alt_battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" else: alt_model_max += alt_SOC[t] == alt_SOC[t - 1] + alt_battery_charge[t] * battery_efficiency - alt_battery_discharge[t] * (1 / battery_efficiency), f"SOCUpdate_{t}" alt_model_max += alt_SOC[t] <= alt_battery_capacity, f"SOCUpperBound_{t}" # Thermal generation cannot exceed capacity alt_model_max += alt_thermal_generation[t] <= alt_capacity[(r, 'thermal')], f"ThermalCapacityConstraint_{t}" # Nuclear generation is fixed at capacity * capacity factor alt_model_max += alt_nuclear_generation[t] == alt_capacity[(r, 'nuclear')] * nuclear_cf, f"NuclearGenerationConstraint_{t}" # Capacity range constraints alt_model_max += alt_capacity[('region1', 'solar')] >= solar_range[0] alt_model_max += alt_capacity[('region1', 'solar')] <= solar_range[1] alt_model_max += alt_capacity[('region1', 'onshore_wind')] >= wind_range[0] alt_model_max += alt_capacity[('region1', 'onshore_wind')] <= wind_range[1] alt_model_max += alt_capacity[('region1', 'offshore_wind')] >= offshore_wind_range[0] alt_model_max += alt_capacity[('region1', 'offshore_wind')] <= offshore_wind_range[1] alt_model_max += alt_capacity[('region1', 'river')] >= river_range[0] alt_model_max += alt_capacity[('region1', 'river')] <= river_range[1] alt_model_max += alt_capacity[('region1', 'nuclear')] >= nuclear_range[0] alt_model_max += alt_capacity[('region1', 'nuclear')] <= nuclear_range[1] alt_model_max += alt_capacity[('region1', 'thermal')] >= thermal_range[0] alt_model_max += alt_capacity[('region1', 'thermal')] <= thermal_range[1] # Maximize the capacity of the selected technology alt_model_max += -alt_capacity[('region1', tech)], f"Maximize_{tech}_Capacity" # Solve the alternative model alt_model_max.solve() if pulp.LpStatus[alt_model_max.status] == 'Optimal': solution = {g: alt_capacity[('region1', g)].varValue for g in technologies} solution['battery_capacity'] = alt_battery_capacity.varValue # Calculate total OPEX for the solution total_opex_value = sum(alt_thermal_generation[t].varValue * opex_cost['thermal'] + alt_nuclear_generation[t].varValue * opex_cost['nuclear'] for t in time_steps) alternative_solutions.append({ 'threshold': threshold, 'type': 'max', 'technology': tech, 'solution': solution, 'total_cost': pulp.value(alt_model_max.objective), 'total_opex': total_opex_value }) return alternative_solutions, capacity_cost, opex_cost, demand, battery_cost_per_mwh, nuclear_cf # Streamlit UI setup st.set_page_config(page_title='Energy System Optimization with MGA', layout='wide') st.title('Modeling to Generate Alternatives (MGA) in Energy System Optimization') # Sidebar Inputs with st.sidebar: st.header('Input Parameters') city_code = st.text_input("Enter City Code", value="999999") solar_cost = st.number_input("Solar Capacity Cost (million ¥/kW)", value= 0.15) onshore_wind_cost = st.number_input("Onshore Wind Capacity Cost (million ¥/kW)", value=0.2) offshore_wind_cost = st.number_input("Offshore Wind Capacity Cost (million ¥/kW)", value=0.4) river_cost = st.number_input("River Capacity Cost (million ¥/kW)", value= 0.3) battery_cost = st.number_input("Battery Cost (million ¥/kWh)", value=0.3) yearly_demand = st.number_input("Yearly Power Demand (TWh/year)", 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("River Capacity Range (MW)", 0, 10000, (0, 10000)) nuclear_range = st.slider("Nuclear Capacity Range (MW)", 0, 10000, (0, 10000)) thermal_range = st.slider("Thermal Capacity Range (MW)", 0, 10000, (0, 10000)) nuclear_cf = st.number_input("Nuclear Capacity Factor (0-1)", value=0.9) nuclear_cost_capex = st.number_input("Nuclear Capacity Cost (million ¥/kW)", value=0.5) nuclear_cost_opex = st.number_input("Nuclear OPEX Cost (million ¥/kWh)", value=0.00002) thermal_cost_capex = st.number_input("Thermal Capacity Cost (million ¥/kW)", value=0.1) thermal_cost_opex = st.number_input("Thermal OPEX Cost (million ¥/kWh)", value=0.00005) thresholds = st.multiselect( "Select MGA Cost Deviation Thresholds (%)", list(np.arange(0, 11, 0.5)), default=[0, 5, 10] ) selected_technologies = st.multiselect( "Select Technologies to Optimize", ['solar', 'onshore_wind', 'offshore_wind', 'river', 'nuclear', 'thermal'], default=['solar', 'onshore_wind', 'offshore_wind', 'river', 'nuclear', 'thermal'] ) if st.button("Run MGA Optimization"): alternative_solutions, capacity_cost, opex_cost, demand, battery_cost_per_mwh, nuclear_cf = optimize_energy_system( city_code, solar_cost, onshore_wind_cost, offshore_wind_cost, river_cost, battery_cost, yearly_demand, solar_range, wind_range, river_range, offshore_wind_range, nuclear_range, thermal_range, nuclear_cf, [t / 100 for t in thresholds], selected_technologies, nuclear_cost_capex, nuclear_cost_opex, thermal_cost_capex, thermal_cost_opex ) if alternative_solutions: # Cost stacking data cost_data = [] for sol in alternative_solutions: tech_costs = {g: sol['solution'][g] * capacity_cost[g] for g in capacity_cost} tech_costs['battery'] = sol['solution']['battery_capacity'] * battery_cost_per_mwh tech_costs['opex'] = sol['total_opex'] cost_data.append({ 'threshold': sol['threshold'] * 100, 'type': sol['type'], 'technology': sol['technology'], 'total_cost': sol['total_cost'], **tech_costs }) cost_df = pd.DataFrame(cost_data) cost_melted = cost_df.melt(id_vars=['threshold', 'type', 'technology', 'total_cost'], value_vars=list(capacity_cost.keys()) + ['battery', 'opex'], var_name='Tech', value_name='Cost') fig_cost = px.bar(cost_melted, x='threshold', y='Cost', color='Tech', title="Cost Breakdown by Technology and Threshold", barmode='stack') fig_cost.update_layout(xaxis_title='Threshold (%)', yaxis_title='Total Cost (¥)') st.plotly_chart(fig_cost, use_container_width=True) # Colors for technologies colors = { 'solar': 'gold', 'onshore_wind': 'skyblue', 'offshore_wind': 'lightgreen', 'river': 'salmon', 'nuclear': 'violet', 'thermal': 'grey', 'battery': 'orange', 'opex': 'red' } epsilon_values = sorted(list(set(sol['threshold'] * 100 for sol in alternative_solutions))) for tech in selected_technologies: capacity_min = [] capacity_max = [] for epsilon in epsilon_values: capacities = [sol['solution'][tech] for sol in alternative_solutions if sol['technology'] == tech and sol['threshold'] * 100 == epsilon] if capacities: capacity_min.append(min(capacities)) capacity_max.append(max(capacities)) else: capacity_min.append(0) capacity_max.append(0) fig, ax = plt.subplots() ax.fill_between(epsilon_values, capacity_min, capacity_max, color=colors.get(tech, 'grey'), alpha=0.3, label=f"{tech} range") ax.plot(epsilon_values, capacity_min, marker='o', color=colors.get(tech, 'grey'), linestyle='-', linewidth=1.5, label=f"{tech} Min") ax.plot(epsilon_values, capacity_max, marker='o', color=colors.get(tech, 'grey'), linestyle='-', linewidth=1.5, label=f"{tech} Max") ax.set_xlabel(r'$\epsilon$ [%]') ax.set_ylabel(f'{tech.capitalize()} Capacity [MW]') ax.set_title(f'Capacity Range for {tech.capitalize()}') ax.legend() ax.grid(True, linestyle='--', alpha=0.7) st.pyplot(fig) else: st.error("No alternative solutions found.") st.markdown(""" This application uses **Modeling to Generate Alternatives (MGA)** to explore near-optimal solutions in an energy system model that includes renewable energy sources, nuclear power, and thermal power plants. MGA helps to identify alternative configurations that are close to the optimal solution but vary in their specific technological composition, providing flexibility for policy makers and stakeholders who might prioritize factors beyond cost minimization, such as social acceptance or regional preferences. """) st.write("## Objective Function and Cost Minimization") st.markdown(""" In our energy system model, the **objective function** is to minimize the total annual cost of the system, which includes the costs of installing generation capacities (such as solar, wind, hydroelectric, nuclear, and thermal) and storage (batteries), as well as operational expenditures (OPEX) for nuclear and thermal power plants. The objective function is defined as: """) st.latex(r""" \text{Minimize } \quad \sum_{r, g} \text{CAPEX}_{g} \times \text{Capacity}_{r, g} + \sum_{t} \left( \text{OPEX}_{\text{nuclear}} \times \text{Nuclear Generation}_{t} + \text{OPEX}_{\text{thermal}} \times \text{Thermal Generation}_{t} \right) + \text{Battery Cost} \times \text{Battery Capacity} """) st.markdown(""" where: - $r$ represents the region (in this case, a single region), - $g$ represents the generation technology (solar, onshore wind, offshore wind, river, nuclear, thermal), - $\text{CAPEX}_{g}$ is the per-MW capital cost of technology $g$, - $\text{Capacity}_{r, g}$ is the installed capacity of technology $g$ in region $r$, - $\text{OPEX}_{\text{nuclear}}$ and $\text{OPEX}_{\text{thermal}}$ are the operational costs per MWh for nuclear and thermal power plants, respectively, - $\text{Nuclear Generation}_{t}$ and $\text{Thermal Generation}_{t}$ are the generation outputs at time $t$ for nuclear and thermal plants, - $\text{Battery Cost}$ represents the cost per MWh of battery storage, - $\text{Battery Capacity}$ is the total installed battery capacity. """) st.markdown(""" ## What is MGA and Why is it Important? Typically, optimization models produce a **single optimal solution** that minimizes the cost under a given set of constraints. However, in many real-world applications, there are **multiple near-optimal solutions** that achieve similar costs but vary in other characteristics. This diversity is valuable because: - **Flexibility**: Different solutions might be preferable depending on policy objectives, geographic constraints, or social preferences. - **Robustness**: Exploring near-optimal solutions reveals which elements (e.g., specific technologies or infrastructure investments) are consistently essential, regardless of slight variations in cost. MGA addresses this need by generating **alternative solutions** that are close to the optimal cost but differ in technological composition. """) st.write("## How MGA Works: Adding a Cost Constraint") st.markdown(""" To generate alternatives, MGA introduces a **cost tolerance** parameter $\epsilon$, which represents the acceptable increase in total cost relative to the optimal solution. The cost constraint for alternative solutions is expressed as: """) st.latex(r""" \text{Total Cost} \leq (1 + \epsilon) \times \text{Optimal Cost} """) st.markdown(""" where: - $\epsilon$ is the cost deviation percentage (e.g., if $\epsilon = 0.05$, then the solution can be up to 5% more expensive than the optimal cost), - $\text{Optimal Cost}$ is the minimum cost obtained from the initial optimization. This constraint allows for flexibility in cost, enabling the exploration of solutions that are **near-optimal** but differ in terms of installed capacities for each technology. """) st.markdown(""" ### MGA Process in This Application 1. **Initial Optimization**: First, we solve for the optimal solution to obtain the minimal total cost, referred to as $\text{Optimal Cost}$. 2. **Setting the Cost Threshold**: We introduce a range of $\epsilon$ values (0%, 5%, 10%, etc.) to explore how alternative solutions differ as we allow for higher costs. 3. **Minimizing and Maximizing Capacities**: For each selected technology (e.g., solar, wind, nuclear, thermal), we attempt to: - **Minimize the installed capacity** within the allowed cost threshold, identifying configurations with the lowest feasible capacity for that technology. - **Maximize the installed capacity** under the same conditions, exploring configurations with higher reliance on that technology. These steps generate a set of **alternative solutions** that are close in cost but vary significantly in their reliance on each technology, revealing **flexibility** and **trade-offs** in the energy system configuration. """) st.write("## Interpreting the Cost Threshold ($\epsilon$ )") st.markdown(""" The cost threshold parameter ($\epsilon$ ) is crucial in MGA, as it determines the range within which we consider solutions to be "near-optimal." For example: - **$\epsilon = 0\% $**: Only the exact optimal solution is considered. - **$\epsilon = 5\% $**: Solutions within 5% of the optimal cost are considered acceptable, allowing for slightly more flexibility in technology choice. - **$\epsilon = 10\% $**: Solutions within 10% of the optimal cost are allowed, providing even greater flexibility. By exploring a range of $\epsilon$ values, we can see how the system configuration changes as we relax the cost constraint, offering a broader view of feasible solutions. """) st.markdown(""" ## Visualization of Results - **Cost Breakdown**: The total cost of each solution, broken down by technology, helps us see the contribution of each technology to the total cost. - **Capacity Ranges**: For each technology, we plot the minimum and maximum capacities across different $\epsilon$ values, showing the flexibility in system design as cost thresholds change. This visualization provides insights into: - Which technologies are essential (appear consistently in solutions across all $\epsilon$ values), - Which technologies offer flexibility (capacities vary widely as $\epsilon$ increases), - The cost impact of relying more or less on specific technologies. Through MGA, we can make more **informed decisions** about the energy mix and identify robust, flexible strategies that align with broader goals beyond cost minimization. """)