diff --git a/landbosse/landbosse_omdao/landbosse.py b/landbosse/landbosse_omdao/landbosse.py index d1bf9d6f..69ba7315 100644 --- a/landbosse/landbosse_omdao/landbosse.py +++ b/landbosse/landbosse_omdao/landbosse.py @@ -1,23 +1,24 @@ -import openmdao.api as om -from math import ceil -import numpy as np import warnings +from math import ceil -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="numpy.ufunc size changed") - import pandas as pd +import numpy as np +import openmdao.api as om from landbosse.model.Manager import Manager from landbosse.model.DefaultMasterInputDict import DefaultMasterInputDict from landbosse.landbosse_omdao.OpenMDAODataframeCache import OpenMDAODataframeCache from landbosse.landbosse_omdao.WeatherWindowCSVReader import read_weather_window +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="numpy.ufunc size changed") + import pandas as pd + + use_default_component_data = -1.0 class LandBOSSE(om.Group): def setup(self): - # Add a tower section height variable. The default value of 30 m is for transportable tower sections. self.set_input_defaults("tower_section_length_m", 30.0, units="m") self.set_input_defaults("blade_drag_coefficient", use_default_component_data) # Unitless @@ -26,6 +27,7 @@ def setup(self): self.set_input_defaults("blade_offload_hook_height", use_default_component_data, units="m") self.set_input_defaults("blade_offload_cycle_time", use_default_component_data, units="h") self.set_input_defaults("blade_drag_multiplier", use_default_component_data) # Unitless + self.set_input_defaults("blade_surface_area", use_default_component_data, units="m**2") self.set_input_defaults("turbine_spacing_rotor_diameters", 4) self.set_input_defaults("row_spacing_rotor_diameters", 10) @@ -65,6 +67,7 @@ def setup_inputs(self): self.add_input("blade_offload_hook_height", use_default_component_data, units="m") self.add_input("blade_offload_cycle_time", use_default_component_data, units="h") self.add_input("blade_drag_multiplier", use_default_component_data) # Unitless + self.add_input("blade_surface_area", use_default_component_data, units="m**2") # Even though LandBOSSE doesn't use foundation height, TowerSE does, # and foundation height can be used with hub height to calculate @@ -447,7 +450,7 @@ def prepare_master_input_dictionary(self, inputs, discrete_inputs): # Needed to avoid distributed wind keys incomplete_input_dict["road_distributed_wind"] = False - + defaults = DefaultMasterInputDict() master_input_dict = defaults.populate_input_dict(incomplete_input_dict) @@ -648,21 +651,21 @@ def modify_component_lists(self, inputs, discrete_inputs): kg_per_tonne = 1000 # Get the hub height - hub_height_meters = inputs["hub_height_meters"][0] + hub_height_meters = float(inputs["hub_height_meters"]) # Make the nacelle. This does not include the hub or blades. - nacelle_mass_kg = inputs["nacelle_mass"][0] + nacelle_mass_kg = float(inputs["nacelle_mass"]) nacelle = input_components[input_components["Component"].str.startswith("Nacelle")].iloc[0].copy() if inputs["nacelle_mass"] != use_default_component_data: nacelle["Mass tonne"] = nacelle_mass_kg / kg_per_tonne nacelle["Component"] = "Nacelle" - nacelle["Lift height m"] = hub_height_meters + nacelle["Lift height m"] = nacelle["Lever arm m"] = hub_height_meters output_components_list.append(nacelle) # Make the hub - hub_mass_kg = inputs["hub_mass"][0] + hub_mass_kg = float(inputs["hub_mass"]) hub = input_components[input_components["Component"].str.startswith("Hub")].iloc[0].copy() - hub["Lift height m"] = hub_height_meters + hub["Lift height m"] = hub["Lever arm m"] = hub_height_meters if hub_mass_kg != use_default_component_data: hub["Mass tonne"] = hub_mass_kg / kg_per_tonne output_components_list.append(hub) @@ -671,33 +674,35 @@ def modify_component_lists(self, inputs, discrete_inputs): blade = input_components[input_components["Component"].str.startswith("Blade")].iloc[0].copy() # There is always a hub height, so use that as the lift height - blade["Lift height m"] = hub_height_meters + blade["Lift height m"] = blade["Lever arm m"] = hub_height_meters - if inputs["blade_drag_coefficient"][0] != use_default_component_data: - blade["Coeff drag"] = inputs["blade_drag_coefficient"][0] + if float(inputs["blade_drag_coefficient"]) != use_default_component_data: + blade["Coeff drag"] = float(inputs["blade_drag_coefficient"]) - if inputs["blade_lever_arm"][0] != use_default_component_data: - blade["Lever arm m"] = inputs["blade_lever_arm"][0] + if float(inputs["blade_lever_arm"]) != use_default_component_data: + blade["Lever arm m"] = float(inputs["blade_lever_arm"]) - if inputs["blade_install_cycle_time"][0] != use_default_component_data: - blade["Cycle time installation hrs"] = inputs["blade_install_cycle_time"][0] + if float(inputs["blade_install_cycle_time"]) != use_default_component_data: + blade["Cycle time installation hrs"] = float(inputs["blade_install_cycle_time"]) - if inputs["blade_offload_hook_height"][0] != use_default_component_data: + if float(inputs["blade_offload_hook_height"]) != use_default_component_data: blade["Offload hook height m"] = hub_height_meters - if inputs["blade_offload_cycle_time"][0] != use_default_component_data: + if float(inputs["blade_offload_cycle_time"]) != use_default_component_data: blade["Offload cycle time hrs"] = inputs["blade_offload_cycle_time"] - if inputs["blade_drag_multiplier"][0] != use_default_component_data: + if float(inputs["blade_drag_multiplier"]) != use_default_component_data: blade["Multiplier drag rotor"] = inputs["blade_drag_multiplier"] - if inputs["blade_mass"][0] != use_default_component_data: - blade["Mass tonne"] = inputs["blade_mass"][0] / kg_per_tonne + if float(inputs["blade_mass"]) != use_default_component_data: + blade["Mass tonne"] = float(inputs["blade_mass"]) / kg_per_tonne + + if float(inputs["blade_surface_area"]) != use_default_component_data: + blade["Surface area sq m"] = float(inputs["blade_surface_area"]) # Assume that number_of_blades always has a reasonable value. It's # default count when the discrete input is declared of 3 is always # reasonable unless overridden by another input. - number_of_blades = discrete_inputs["number_of_blades"] for i in range(number_of_blades): component = f"Blade {i + 1}" @@ -706,8 +711,8 @@ def modify_component_lists(self, inputs, discrete_inputs): output_components_list.append(blade_i) # Make tower sections - tower_mass_tonnes = inputs["tower_mass"][0] / kg_per_tonne - tower_height_m = hub_height_meters - inputs["foundation_height"][0] + tower_mass_tonnes = float(inputs["tower_mass"]) / kg_per_tonne + tower_height_m = hub_height_meters - float(inputs["foundation_height"]) default_tower_section = input_components[input_components["Component"].str.startswith("Tower")].iloc[0] tower_sections = self.make_tower_sections(tower_mass_tonnes, tower_height_m, default_tower_section) output_components_list.extend(tower_sections) @@ -767,7 +772,7 @@ def make_tower_sections(tower_mass_tonnes, tower_height_m, default_tower_section tower_section_mass = tower_mass_tonnes / number_of_sections - tower_section_surface_area_m2 = np.pi * tower_section_height_m * (tower_radius ** 2) + tower_section_surface_area_m2 = np.pi * tower_section_height_m * (tower_radius**2) sections = [] for i in range(number_of_sections): diff --git a/landbosse/model/CollectionCost.py b/landbosse/model/CollectionCost.py index 6b087104..0527365f 100644 --- a/landbosse/model/CollectionCost.py +++ b/landbosse/model/CollectionCost.py @@ -677,7 +677,7 @@ def calculate_weather_delay(self, weather_delay_input_data, weather_delay_output # if greater than 4 hour delay, then shut down for full day (10 hours) wind_delay[(wind_delay > 4)] = 10 - weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum()) + weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum().iloc[0]) return weather_delay_output_data diff --git a/landbosse/model/FoundationCost.py b/landbosse/model/FoundationCost.py index 3ca0bcb6..3d877762 100644 --- a/landbosse/model/FoundationCost.py +++ b/landbosse/model/FoundationCost.py @@ -553,7 +553,7 @@ def calculate_weather_delay(self, weather_delay_input_data, weather_delay_output # if greater than 4 hour delay, then shut down for full day (10 hours) wind_delay[(wind_delay > 4)] = 10 - weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum()) + weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum().iloc[0]) return weather_delay_output_data diff --git a/landbosse/model/Manager.py b/landbosse/model/Manager.py index aa31448e..d99c9f88 100644 --- a/landbosse/model/Manager.py +++ b/landbosse/model/Manager.py @@ -4,9 +4,10 @@ from .ManagementCost import ManagementCost from .FoundationCost import FoundationCost from .SubstationCost import SubstationCost +from .TransportCost import TransportCost from .GridConnectionCost import GridConnectionCost from .SitePreparationCost import SitePreparationCost -from .CollectionCost import Cable, Array, ArraySystem +from .CollectionCost import ArraySystem from .ErectionCost import ErectionCost from .DevelopmentCost import DevelopmentCost @@ -58,6 +59,9 @@ def execute_landbosse(self, project_name): substation_cost = SubstationCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) substation_cost.run_module() + transport_cost = TransportCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) + transport_cost.run_module() + transdist_cost = GridConnectionCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) transdist_cost.run_module() @@ -96,6 +100,7 @@ def execute_landbosse(self, project_name): self.output_dict['total_road_cost'], self.output_dict['total_transdist_cost'], self.output_dict['total_substation_cost'], + self.output_dict['total_transport_cost'], self.output_dict['total_foundation_cost'], self.output_dict['total_erection_cost'], self.output_dict['total_development_cost'], diff --git a/landbosse/model/SitePreparationCost.py b/landbosse/model/SitePreparationCost.py index 6f9536ef..6d534e1e 100644 --- a/landbosse/model/SitePreparationCost.py +++ b/landbosse/model/SitePreparationCost.py @@ -1,10 +1,9 @@ -import pandas as pd import numpy as np import math from .WeatherDelay import WeatherDelay as WD import traceback from .CostModule import CostModule - +import pandas as pd class SitePreparationCost(CostModule): """ @@ -420,7 +419,7 @@ def calculate_weather_delay(self, weather_delay_input_data, weather_delay_output # if greater than 4 hour delay, then shut down for full day (10 hours) wind_delay[(wind_delay > 4)] = 10 - weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum()) + weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum().iloc[0]) return weather_delay_output_data @@ -541,7 +540,7 @@ def calculate_costs(self, calculate_cost_input_dict, calculate_cost_output_dict) if calculate_cost_input_dict['road_distributed_wind'] and \ calculate_cost_input_dict['turbine_rating_MW'] >= 0.1: - labor_for_new_roads_cost_usd = (labor_data['Cost USD'].sum()) + \ + labor_for_new_roads_cost_usd = labor_data['Cost USD'].sum() + \ calculate_cost_output_dict['managament_crew_cost_before_wind_delay'] labor_for_new_and_old_roads_cost_usd = self.new_and_existing_total_road_cost(labor_for_new_roads_cost_usd) @@ -551,7 +550,7 @@ def calculate_costs(self, calculate_cost_input_dict, calculate_cost_output_dict) elif calculate_cost_input_dict['road_distributed_wind'] and \ calculate_cost_input_dict['turbine_rating_MW'] < 0.1: # small DW - labor_for_new_roads_cost_usd = (labor_data['Cost USD'].sum()) + labor_for_new_roads_cost_usd = labor_data['Cost USD'].sum() labor_for_new_and_old_roads_cost_usd = self.new_and_existing_total_road_cost(labor_for_new_roads_cost_usd) labor_costs = pd.DataFrame([['Labor', float(labor_for_new_and_old_roads_cost_usd), 'Small DW Roads']], diff --git a/landbosse/model/TransportCost.py b/landbosse/model/TransportCost.py new file mode 100644 index 00000000..c564ffe8 --- /dev/null +++ b/landbosse/model/TransportCost.py @@ -0,0 +1,160 @@ +import traceback + +import pandas as pd +import scipy.interpolate +from .CostModule import CostModule +import numpy as np + +class TransportCost(CostModule): + """ + **TransportCost.py** + + Calculates the costs associated with transportation for land-based wind projects *(module is currently based on curve fit of empirical data)* + + Get number of blades, nacelles + + Return total trasnport costs + + \n\n**Keys in the input dictionary are the following:** + + rotor_diameter_m + (float) Determines the approximate blade length [in m] + + number_turbines + (int) Number of turbines + + \n\n**Keys in the output dictionary are the following:** + + transport_cost + (float) cost of transportation [in USD] + + + """ + + def __init__(self, input_dict, output_dict, project_name): + """ + Parameters + ---------- + input_dict : dict + The input dictionary with key value pairs described in the + class documentation + + output_dict : dict + The output dictionary with key value pairs as found on the + output documentation. + """ + self.input_dict = input_dict + self.output_dict = output_dict + self.project_name = project_name + + def calculate_costs(self, calculate_costs_input_dict, calculate_costs_output_dict): + """ + Function to calculate Transport Cost in USD + + Parameters + ------- + interconnect_voltage_kV + (in kV) + + project_size_megawatts + (in MW) + + + Returns: + ------- + substation_cost + (in USD) + + """ + + blade_length = 0.5 * calculate_costs_input_dict["rotor_diameter_m"] + n_turb = calculate_costs_input_dict["num_turbines"] + # Transport cost is $/blade * nblades + infrastructure costs + cost for tower & nacelle + # Blade transport from emp.lbl.gov/publications/supersized-wind-turbine-blade-study + xlen = np.array([-500., 0., 65., 75., 95., 115.]) + ycost = 1e3 * np.array([0.0, 0.0, 52., 70., 120., 171.]) + yinfra = 1e6 * np.array([0.0, 0.0, 0.0, 0.2, 1.0, 5.0]) + + f_blade = scipy.interpolate.interp1d(xlen, ycost, fill_value='extrapolate', assume_sorted=True) + cost_per_blade = f_blade(blade_length) + + f_infra = scipy.interpolate.interp1d(xlen, yinfra, fill_value='extrapolate', assume_sorted=True) + cost_infra = f_infra(blade_length) + + # Multiply by 4x for 3 blades + 1 tower + calculate_costs_output_dict["transport_cost_usd"] = 4 * cost_per_blade * n_turb + cost_infra + + calculate_costs_output_dict["transport_cost_output_df"] = pd.DataFrame( + [["Other", calculate_costs_output_dict["transport_cost_usd"], "Transport"]], + columns=["Type of cost", "Cost USD", "Phase of construction"], + ) + + calculate_costs_output_dict["total_transport_cost"] = calculate_costs_output_dict["transport_cost_output_df"] + + return calculate_costs_output_dict["transport_cost_output_df"] + + def outputs_for_detailed_tab(self, input_dict, output_dict): + """ + Creates a list of dictionaries which can be used on their own or + used to make a dataframe. + + Must be called after self.run_module() + + Returns + ------- + list(dict) + A list of dicts, with each dict representing a row of the data. + """ + result = [] + module = type(self).__name__ + + for row in self.output_dict["transport_cost_output_df"].itertuples(): + dashed_row = "{} <--> {} <--> {}".format(row[1], row[3], np.ceil(row[2])) + result.append( + { + "unit": "", + "type": "dataframe", + "variable_df_key_col_name": "Type of Cost <--> Phase of Construction <--> Cost in USD ", + "value": dashed_row, + "last_number": row[2], + } + ) + + for _dict in result: + _dict["project_id_with_serial"] = self.project_name + _dict["module"] = module + + self.output_dict["transport_cost_csv"] = result + return result + + def run_module(self): + """ + Runs the TransportCost module and populates the IO dictionaries with calculated values. + + Parameters + ---------- + + + Returns + ------- + tuple + First element of tuple contains a 0 or 1. 0 means no errors happened and + 1 means an error happened and the module failed to run. The second element + either returns a 0 if the module ran successfully, or it returns the error + raised that caused the failure. + + """ + try: + self.calculate_costs(self.input_dict, self.output_dict) + self.outputs_for_detailed_tab(self.input_dict, self.output_dict) + # self.outputs_for_module_type_operation(self.input_dict, self.output_dict) + self.output_dict["transport_module_type_operation"] = self.outputs_for_costs_by_module_type_operation( + input_df=self.output_dict["transport_cost_output_df"], + project_id=self.project_name, + total_or_turbine=True, + ) + return 0, 0 + except Exception as error: + traceback.print_exc() + print(f"Fail {self.project_name} TransportCost") + return 1, error diff --git a/project_input_template/landbosse-expected-validation-data.xlsx b/project_input_template/landbosse-expected-validation-data.xlsx index 9db03ccb..d74f8249 100644 Binary files a/project_input_template/landbosse-expected-validation-data.xlsx and b/project_input_template/landbosse-expected-validation-data.xlsx differ diff --git a/project_input_template/project_data/ge15_expected_validation.xlsx b/project_input_template/project_data/ge15_expected_validation.xlsx index b804fac0..dcf1ec6f 100644 Binary files a/project_input_template/project_data/ge15_expected_validation.xlsx and b/project_input_template/project_data/ge15_expected_validation.xlsx differ diff --git a/project_input_template/project_data/ge15_public.xlsx b/project_input_template/project_data/ge15_public.xlsx index 391baaf9..2ea83dc1 100644 Binary files a/project_input_template/project_data/ge15_public.xlsx and b/project_input_template/project_data/ge15_public.xlsx differ