Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uniform output #312

Merged
merged 11 commits into from
May 8, 2024
106 changes: 99 additions & 7 deletions greenheart/simulation/greenheart_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@

pd.options.mode.chained_assignment = None # default='warn'

from hopp.simulation import HoppInterface
from ProFAST import ProFAST

from greenheart.simulation.technologies.ammonia.ammonia import (
run_ammonia_full_model,
run_ammonia_full_model,
AmmoniaCostModelOutputs,
AmmoniaFinanceModelOutputs,
AmmoniaCapacityModelOutputs
)
from greenheart.simulation.technologies.steel.steel import (
run_steel_full_model,
SteelCostModelOutputs,
SteelFinanceModelOutputs,
SteelCapacityModelOutputs
)

# visualization imports
Expand Down Expand Up @@ -161,6 +170,66 @@ def __attrs_post_init__(self):
self.orbit_config["plant"]["capacity"] * 1e6
)

@define
class GreenHeartSimulationOutput:
"""This is a dataclass to contain the outputs from GreenHEART

Args:
greenheart_config (GreenHeartSimulationConfig): all inputs to the greenheart simulation
hopp_interface (HoppInterface): the hopp interface created and used by GreenHEART in the simulation
profast_lcoe (ProFAST): the profast instance used for the lcoe calculations
profast_lcoh (ProFAST): the profast instance used for the lcoh calculations
profast_lcoh (ProFAST): the profast instance used for the lcoh calculations if hydrogen were produced only from the grid
lcoe (float): levelized cost of energy (electricity)
lcoh (float): levelized cost of hydrogen
lcoh_grid_only (float): levelized cost of hydrogen if produced only from the grid
hopp_results (dict): results from the hopp simulation
electrolyzer_physics_results (dict): results of the electrolysis simulation
capex_breakdown (dict): overnight capex broken down by technology
opex_breakdown_annual (dict): annual operational expenditures broken down by technology
annual_energy_breakdown (dict): annual energy generation and usage broken down by technology
hourly_energy_breakdown (dict): hourly energy generation and usage broken down by technology
remaining_power_profile (np.ndarray): unused power (hourly)
steel_capacity (Optional[SteelCapacityModelOutputs]): steel capacity information
steel_costs (Optional[SteelCostModelOutputs]): steel cost information
steel_finance (Optional[SteelFinanceModelOutputs]): steel financial information
ammonia_capacity (Optional[AmmoniaCapacityModelOutputs]): ammonia capacity information
ammonia_costs (Optional[AmmoniaCostModelOutputs]): ammonia cost information
ammonia_finance (Optional[AmmoniaFinanceModelOutputs]): ammonia finance information
"""

# detailed simulation information
greenheart_config: GreenHeartSimulationConfig
hopp_interface: HoppInterface

# detailed financial outputs
profast_lcoe: ProFAST
profast_lcoh: ProFAST
profast_lcoh_grid_only: ProFAST

# high-level results
lcoe: float
lcoh: float
lcoh_grid_only: float

# detailed output information
hopp_results: dict
electrolyzer_physics_results: dict
capex_breakdown: dict
opex_breakdown_annual: dict
annual_energy_breakdown: dict
hourly_energy_breakdown: dict
remaining_power_profile: np.ndarray

# optional outputs
steel_capacity: Optional[SteelCapacityModelOutputs] = field(default=None)
steel_costs: Optional[SteelCostModelOutputs] = field(default=None)
steel_finance: Optional[SteelFinanceModelOutputs] = field(default=None)

ammonia_capacity: Optional[AmmoniaCapacityModelOutputs] = field(default=None)
ammonia_costs: Optional[AmmoniaCostModelOutputs] = field(default=None)
ammonia_finance: Optional[AmmoniaFinanceModelOutputs] = field(default=None)

def setup_greenheart_simulation(config: GreenHeartSimulationConfig):

# run orbit for wind plant construction and other costs
Expand Down Expand Up @@ -520,7 +589,7 @@ def energy_internals(
h2_storage_results,
total_accessory_power_renewable_kw,
total_accessory_power_grid_kw,
remaining_power_profile,
remaining_power_profile
)

# define function to provide to the brent solver
Expand Down Expand Up @@ -733,7 +802,7 @@ def simple_solver(initial_guess=0.0):
"hydrogen_amount_kgpy"
] = hydrogen_amount_kgpy

_, _, steel_finance = run_steel_full_model(config.greenheart_config, save_plots=config.save_plots, show_plots=config.show_plots, output_dir=config.output_dir, design_scenario_id=config.design_scenario["id"])
steel_capacity, steel_costs, steel_finance = run_steel_full_model(config.greenheart_config, save_plots=config.save_plots, show_plots=config.show_plots, output_dir=config.output_dir, design_scenario_id=config.design_scenario["id"])

else:
steel_finance = {}
Expand All @@ -751,14 +820,14 @@ def simple_solver(initial_guess=0.0):
"hydrogen_amount_kgpy"
] = hydrogen_amount_kgpy

_, _, ammonia_finance = run_ammonia_full_model(config.greenheart_config, save_plots=config.save_plots, show_plots=config.show_plots, output_dir=config.output_dir, design_scenario_id=config.design_scenario["id"])
ammonia_capacity, ammonia_costs, ammonia_finance = run_ammonia_full_model(config.greenheart_config, save_plots=config.save_plots, show_plots=config.show_plots, output_dir=config.output_dir, design_scenario_id=config.design_scenario["id"])

else:
ammonia_finance = {}

################# end OSW intermediate calculations
if config.post_processing:
power_breakdown = he_util.post_process_simulation(
annual_energy_breakdown, hourly_energy_breakdown = he_util.post_process_simulation(
lcoe,
lcoh,
pf_lcoh,
Expand Down Expand Up @@ -808,7 +877,7 @@ def simple_solver(initial_guess=0.0):
pf_lcoh,
electrolyzer_physics_results,
pf_lcoe,
power_breakdown,
annual_energy_breakdown,
)
elif config.output_level == 4:
return lcoe, lcoh, lcoh_grid_only
Expand All @@ -818,7 +887,30 @@ def simple_solver(initial_guess=0.0):
return hopp_results, electrolyzer_physics_results, remaining_power_profile
elif config.output_level == 7:
return lcoe, lcoh, steel_finance, ammonia_finance

elif config.output_level == 8:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about getting rid of output_level completely and just returning the data class? I know that it might mess a few folks up right now but lots of other breaking changes are happening on this branch and I think using data classes will help improve the greenheart simulation generalization

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to avoid running post-processing and passing a lot of extra stuff around during optimization runs. So, for now anyway, I would like to keep the output levels. We should probably make output level 8 the de facto in our example yamls though to encourage people to use that option

return GreenHeartSimulationOutput(
config,
hi,
pf_lcoe,
pf_lcoh,
pf_grid_only,
lcoe,
lcoh,
lcoh_grid_only,
hopp_results,
electrolyzer_physics_results,
capex_breakdown,
opex_breakdown_annual,
annual_energy_breakdown,
hourly_energy_breakdown,
remaining_power_profile,
steel_capacity = None if "steel" not in config.greenheart_config else steel_capacity,
steel_costs = None if "steel" not in config.greenheart_config else steel_costs,
steel_finance = None if "steel" not in config.greenheart_config else steel_finance,
ammonia_capacity = None if "ammonia" not in config.greenheart_config else ammonia_capacity,
ammonia_costs = None if "ammonia" not in config.greenheart_config else ammonia_costs,
ammonia_finance = None if "ammonia" not in config.greenheart_config else ammonia_finance
)

def run_sweeps(simulate=False, verbose=True, show_plots=True, use_profast=True, output_dir="output/"):

Expand Down
25 changes: 11 additions & 14 deletions greenheart/tools/eco/electrolysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def run_electrolyzer_physics(
ax[0, 0].plot(wind_speed)
convolved_wind_speed = np.convolve(wind_speed, np.ones(N) / (N), mode="valid")
ave_x = range(N, len(convolved_wind_speed) + N)

ax[0, 1].plot(ave_x, convolved_wind_speed)
ax[0, 0].set(ylabel="Wind\n(m/s)", ylim=[0, 30], xlim=[0, len(wind_speed)])
tick_spacing = 10
Expand All @@ -199,11 +199,14 @@ def run_electrolyzer_physics(
y = greenheart_config["electrolyzer"]["rating"]
ax[1, 0].plot(energy_to_electrolyzer_kw * 1e-3)
ax[1, 0].axhline(y=y, color="r", linestyle="--", label="Nameplate Capacity")
ax[1, 1].plot(
ave_x[:-1],
np.convolve(

convolved_energy_to_electrolyzer = np.convolve(
energy_to_electrolyzer_kw * 1e-3, np.ones(N) / (N), mode="valid"
),
)

ax[1, 1].plot(
ave_x,
convolved_energy_to_electrolyzer,
)
ax[1, 1].axhline(y=y, color="r", linestyle="--", label="Nameplate Capacity")
ax[1, 0].set(
Expand All @@ -220,16 +223,10 @@ def run_electrolyzer_physics(
]
* 1e-3
)
convolved_hydrogen_production = np.convolve(electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"]*1e-3,np.ones(N) / (N), mode="valid")
ax[2, 1].plot(
ave_x[:-1],
np.convolve(
electrolyzer_physics_results["H2_Results"][
"Hydrogen Hourly Production [kg/hr]"
]
* 1e-3,
np.ones(N) / (N),
mode="valid",
),
ave_x,
convolved_hydrogen_production,
)
tick_spacing = 2
ax[2, 0].set(
Expand Down
2 changes: 1 addition & 1 deletion greenheart/tools/eco/hopp_mgmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def run_hopp(hi, project_lifetime, verbose=False):
"hopp_interface": hi,
"hybrid_plant": hi.system,
"combined_hybrid_power_production_hopp": \
hi.system.grid._system_model.Outputs.system_pre_interconnect_kwac[0:8759],
hi.system.grid._system_model.Outputs.system_pre_interconnect_kwac[0:8760],
"combined_hybrid_curtailment_hopp": hi.system.grid.generation_curtailed,
"energy_shortfall_hopp": hi.system.grid.missed_load,
"annual_energies": hi.system.annual_energies,
Expand Down
47 changes: 32 additions & 15 deletions greenheart/tools/eco/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,11 +794,11 @@ def add_turbines(ax, turbine_x, turbine_y, radius, color):
e_side_x = electrolyzer_area / e_side_y
d_side_y = equipment_platform_side_length
d_side_x = desal_equipment_area / d_side_y
ex = dx + d_side_x
ey = dy
electrolyzer_x = dx + d_side_x
electrolyzer_y = dy

electrolyzer_patch = patches.Rectangle(
(ex, ey),
(electrolyzer_x, electrolyzer_y),
e_side_x,
e_side_y,
color=electrolyzer_color,
Expand Down Expand Up @@ -1293,10 +1293,12 @@ def add_turbines(ax, turbine_x, turbine_y, radius, color):
return 0


def save_power_series(
hybrid_plant: HoppInterface.system, ax=None, simulation_length=8760, output_dir="./output/",
def save_energy_flows(
hybrid_plant: HoppInterface.system, electrolyzer_physics_results, solver_results, hours, ax=None, simulation_length=8760, output_dir="./output/",
):



if ax == None:
fig, ax = plt.subplots(1)

Expand All @@ -1305,30 +1307,41 @@ def save_power_series(
solar_plant_power = np.array(
hybrid_plant.pv.generation_profile[0:simulation_length]
)
output.update({"pv": solar_plant_power})
output.update({"pv generation [kW]": solar_plant_power})
if hybrid_plant.wind:
wind_plant_power = np.array(
hybrid_plant.wind.generation_profile[0:simulation_length]
)
output.update({"wind": wind_plant_power})
output.update({"wind generation [kW]": wind_plant_power})
if hybrid_plant.wave:
wave_plant_power = np.array(
hybrid_plant.wave.generation_profile[0:simulation_length]
)
output.update({"wave": wave_plant_power})
output.update({"wave generation [kW]": wave_plant_power})
if hybrid_plant.battery:
battery_power_out = hybrid_plant.battery.outputs.dispatch_P
output.update({"battery": battery_power_out})

battery_power_out_mw = hybrid_plant.battery.outputs.P
output.update({"battery discharge [kW]": [(int(p>0))*p*1E3 for p in battery_power_out_mw]}) # convert from MW to kW and extract only discharging
output.update({"battery charge [kW]": [-(int(p<0))*p*1E3 for p in battery_power_out_mw]}) # convert from MW to kW and extract only charging
output.update({"battery state of charge [%]": hybrid_plant.battery.outputs.dispatch_SOC})

output.update({"total renewable energy production hourly [kW]": [solver_results[0]]*simulation_length})
output.update({"grid energy usage hourly [kW]": [solver_results[1]]*simulation_length})
output.update({"desal energy hourly [kW]": [solver_results[2]]*simulation_length})
output.update({"electrolyzer energy hourly [kW]": electrolyzer_physics_results["power_to_electrolyzer_kw"]})
output.update({"transport compressor energy hourly [kW]": [solver_results[3]]*simulation_length})
output.update({"storage energy hourly [kW]": [solver_results[4]]*simulation_length})
output.update({"h2 production hourly [kg]": electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"]})

df = pd.DataFrame.from_dict(output)

filepath = os.path.abspath(output_dir + "data/production/")

if not os.path.exists(filepath):
os.makedirs(filepath)

df.to_csv(os.path.join(filepath, "power_series.csv"))
df.to_csv(os.path.join(filepath, "energy_flows.csv"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as we keep generating more figures and data files, we should try to settle on what sort of information should be included in the file name. This format only works in the single run case which can make running sweeps difficult

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, data and figure handling is a mess and should be cleaned up, unified, and generalized.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think should be made an issue so we can address it, but not in this PR


return 0
return output


# set up function to post-process HOPP results
Expand Down Expand Up @@ -1576,6 +1589,10 @@ def post_process_simulation(
)

# save production information
save_power_series(hopp_results["hybrid_plant"])
hourly_energy_breakdown = save_energy_flows(hopp_results["hybrid_plant"], electrolyzer_physics_results, solver_results, hours)

# save hydrogen information
key = "Hydrogen Hourly Production [kg/hr]"
np.savetxt(output_dir+"h2_usage", electrolyzer_physics_results["H2_Results"][key], header="# "+key)

return annual_energy_breakdown
return annual_energy_breakdown, hourly_energy_breakdown