diff --git a/landbosse/excelio/XlsxFileOperations.py b/landbosse/excelio/XlsxFileOperations.py index 162ada72..a89023f1 100644 --- a/landbosse/excelio/XlsxFileOperations.py +++ b/landbosse/excelio/XlsxFileOperations.py @@ -70,6 +70,8 @@ def get_input_output_paths_from_argv_or_env(self): input_path_from_env = os.environ.get('LANDBOSSE_INPUT_DIR', 'input') output_path_from_env = os.environ.get('LANDBOSSE_OUTPUT_DIR', 'output') + # input_path_from_env = os.environ['LANDBOSSE_INPUT_DIR'] if 'LANDBOSSE_INPUT_DIR' in os.environ else 'input' + # output_path_from_env = os.environ['LANDBOSSE_OUTPUT_DIR'] if 'LANDBOSSE_OUTPUT_DIR' in os.environ else 'output' # input and output paths from command line are initially set to None # to indicate they have not been found yet. diff --git a/landbosse/excelio/XlsxReader.py b/landbosse/excelio/XlsxReader.py index 5af3a082..2c5abe93 100644 --- a/landbosse/excelio/XlsxReader.py +++ b/landbosse/excelio/XlsxReader.py @@ -370,15 +370,33 @@ def create_master_input_dictionary(self, project_data_dataframes, project_parame labor_cost_multiplier = project_parameters['Labor cost multiplier'] self.apply_labor_multiplier_to_project_data_dict(project_data_dataframes, labor_cost_multiplier) - erection_input_worksheets = [ - 'crane_specs', - 'equip', - 'crew', - 'equip_price', - 'crew_price', - 'material_price', - 'components' - ] + # use turbine spacings for auto mode, and turbine locations for manual mode + if project_parameters['Collection mode'] == 'manual': + incomplete_input_dict['collection_layout'] = project_data_dataframes['collection_layout'] + erection_input_worksheets = [ + 'crane_specs', + 'equip', + 'crew', + 'equip_price', + 'crew_price', + 'material_price', + 'components', + 'collection_layout' + ] + else: + incomplete_input_dict['row_spacing_rotor_diameters'] = project_parameters['Row spacing (times rotor diameter)'] + incomplete_input_dict['turbine_spacing_rotor_diameters'] = project_parameters['Turbine spacing (times rotor diameter)'] + erection_input_worksheets = [ + 'crane_specs', + 'equip', + 'crew', + 'equip_price', + 'crew_price', + 'material_price', + 'components' + ] + if project_parameters['Collection mode'] == 'manual' and (len(project_data_dataframes['collection_layout']['Adjacency matrix']) != (project_parameters['Number of turbines']+1)): + exit('ERROR: mismatch between # turbines and # turbine locations') erection_project_data_dict = dict() for worksheet in erection_input_worksheets: @@ -435,7 +453,6 @@ def create_master_input_dictionary(self, project_data_dataframes, project_parame project_parameters['Breakpoint between base and topping (percent)'] incomplete_input_dict['fuel_usd_per_gal'] = project_parameters['Fuel cost USD per gal'] incomplete_input_dict['rate_of_deliveries'] = project_parameters['Rate of deliveries (turbines per week)'] - incomplete_input_dict['turbine_spacing_rotor_diameters'] = project_parameters['Turbine spacing (times rotor diameter)'] incomplete_input_dict['depth'] = project_parameters['Foundation depth m'] incomplete_input_dict['rated_thrust_N'] = project_parameters['Rated Thrust (N)'] incomplete_input_dict['bearing_pressure_n_m2'] = project_parameters['Bearing Pressure (n/m2)'] @@ -447,9 +464,8 @@ def create_master_input_dictionary(self, project_data_dataframes, project_parame incomplete_input_dict['road_quality'] = project_parameters['Road Quality (0-1)'] incomplete_input_dict['line_frequency_hz'] = project_parameters['Line Frequency (Hz)'] incomplete_input_dict['plant_capacity_MW'] = project_parameters['Turbine rating MW'] * project_parameters['Number of turbines'] - incomplete_input_dict['row_spacing_rotor_diameters'] = project_parameters['Row spacing (times rotor diameter)'] incomplete_input_dict['user_defined_distance_to_grid_connection'] = project_parameters['Flag for user-defined home run trench length (0 = no; 1 = yes)'] - incomplete_input_dict['distance_to_grid_connection_km'] = project_parameters['Combined Homerun Trench Length to Substation (km)'] + incomplete_input_dict['distance_to_grid_connection_mi'] = project_parameters['Distance to interconnect [mi]'] incomplete_input_dict['crew'] = incomplete_input_dict['project_data']['crew'] incomplete_input_dict['crew_cost'] = incomplete_input_dict['project_data']['crew_price'] @@ -462,14 +478,13 @@ def create_master_input_dictionary(self, project_data_dataframes, project_parame incomplete_input_dict['line_frequency_hz'] = project_parameters['Line Frequency (Hz)'] incomplete_input_dict['plant_capacity_MW'] = project_parameters['Turbine rating MW'] * project_parameters['Number of turbines'] - incomplete_input_dict['row_spacing_rotor_diameters'] = project_parameters['Row spacing (times rotor diameter)'] incomplete_input_dict['user_defined_home_run_trench'] = project_parameters[ 'Flag for user-defined home run trench length (0 = no; 1 = yes)'] incomplete_input_dict['trench_len_to_substation_km'] = project_parameters[ 'Combined Homerun Trench Length to Substation (km)'] # Add inputs for transmission & Substation modules: - incomplete_input_dict['distance_to_interconnect_mi'] = project_parameters['Distance to interconnect (miles)'] + incomplete_input_dict['distance_to_interconnect_mi'] = project_parameters['Distance to interconnect [mi]'] incomplete_input_dict['interconnect_voltage_kV'] = project_parameters['Interconnect Voltage (kV)'] new_switchyard = True if project_parameters['New Switchyard (y/n)'] == 'y': @@ -499,6 +514,7 @@ def create_master_input_dictionary(self, project_data_dataframes, project_parame incomplete_input_dict['markup_sales_and_use_tax'] = project_parameters['Markup sales and use tax'] incomplete_input_dict['markup_overhead'] = project_parameters['Markup overhead'] incomplete_input_dict['markup_profit_margin'] = project_parameters['Markup profit margin'] + incomplete_input_dict['collection_mode'] = project_parameters['Collection mode'] # Now fill any missing values with sensible defaults. defaults = DefaultMasterInputDict() @@ -574,7 +590,7 @@ def apply_cost_and_scaling_modifications_to_project_parameters(self, project_par flag_use_user_homerun = project_parameters['Flag for user-defined home run trench length (0 = no; 1 = yes)'] nameplate = project_parameters['Turbine rating MW'] - distance_to_interconnect_mi = 0.0 if project_size_MW <= 20 else (0.009375 * project_size_MW + 0.625) + # distance_to_interconnect_mi = 0.0 if project_size_MW <= 20 else (0.009375 * project_size_MW + 0.625) #TODO decide between user defined dist to interconnect and formula interconnect_voltage_kV = 0.4398 * project_size_MW + 60.204 new_switchyard_y_n = 'n' if project_size_MW <= 40 else 'y' road_length_adder_m = 1e3 if project_size_MW <= 20 else (13.542 * project_size_MW + 1458.3) @@ -596,7 +612,6 @@ def apply_cost_and_scaling_modifications_to_project_parameters(self, project_par project_parameters['Rate of deliveries(turbines per week)'] = rate_deliveries project_parameters['Development labor cost USD'] = development_labor_cost_usd project_parameters['Project size MW'] = project_size_MW - project_parameters['Distance to interconnect (miles)'] = distance_to_interconnect_mi project_parameters['Interconnect Voltage (kV)'] = interconnect_voltage_kV project_parameters['New Switchyard (y/n)'] = new_switchyard_y_n project_parameters['Road length adder (m)'] = road_length_adder_m diff --git a/landbosse/model/CollectionCost.py b/landbosse/model/CollectionCost.py index 53f8cab3..c0265e5f 100644 --- a/landbosse/model/CollectionCost.py +++ b/landbosse/model/CollectionCost.py @@ -16,7 +16,6 @@ import numpy as np import traceback import pandas as pd - from .CostModule import CostModule from .WeatherDelay import WeatherDelay as WD @@ -38,7 +37,7 @@ class Cable: current_capacity : float Cable current rating at 1m burial depth, Amps rated_voltage : float - Cable rated voltage, kV + Cable rated voltage, V ac_resistance : float Cable resistance for AC current, Ohms/km inductance : float @@ -77,8 +76,10 @@ class documentation self.capacitance = cable_specs['Capacitance (nF/km)'] self.cost = cable_specs['Cost (USD/LF)'] self.line_frequency_hz = addl_specs['line_frequency_hz'] - - + self.mode = addl_specs['mode'] + # only include length in cable object if in manual mode. Otherwise Array object specs. length + if self.mode == 'manual': + self.total_length = 0 # Calc additional cable specs self.calc_char_impedance(self.line_frequency_hz) self.calc_power_factor() @@ -167,16 +168,17 @@ def __init__(self, cable_specs, addl_inputs): def calc_max_turb_per_cable(self, addl_inputs): """ Calculate the number of turbines that each cable can support - Parameters ---------- + turbine_ampacity : int + Amperage generated by each turbine at the collection system voltage turbine_rating_MW : int + OR Nameplate capacity of individual turbines """ - turbine_rating_MW = addl_inputs['turbine_rating_MW'] - - self.max_turb_per_cable = np.floor(self.cable_power / turbine_rating_MW) + self.max_turb_per_cable = np.floor(self.current_capacity / addl_inputs['turbine_ampacity']) + # self.max_turb_per_cable = np.floor(self.cable_power / turbine_rating_MW) #TODO figure out if you want to calc with power rating or ampacity!!! def calc_num_turb_per_cable(self, addl_inputs): """ @@ -238,52 +240,126 @@ def calc_turb_section_len(self, turbine_spacing_rotor_diameters, rotor_diameter_ """ self.turb_section_length = (turbine_spacing_rotor_diameters * rotor_diameter_m) / 1000 - return self.turb_section_length - class ArraySystem(CostModule): """ - - \nThis module: - * Calculates cable length to substation - * Calculates number of strings in a subarray - * Calculated number of strings - * Calculates total cable length for each cable type - * Calculates total trench length - * Calculates total collection system cost based on amount of material, amount of labor, price data, cable length, and trench length. - - - **Keys in the input dictionary are the following:** - * Given below are attributes that define each cable type: * conductor_size (int) cross-sectional diameter of cable [in mm] - - - """ - def __init__(self, input_dict, output_dict, project_name): - self.input_dict = input_dict self.output_dict = output_dict self.project_name = project_name self.output_dict['total_cable_len_km'] = 0 self._km_to_LF = 0.0003048 #Units: [km/LF] Conversion factor for converting from km to linear foot. self._total_cable_cost = 0 - self._cable_length_km = dict() + # auto mode: the user defines wind turbine spacing based on rotor diameter. The code creates a grid. + # manual mode: the user defines turbine xy coordinates + self.mode = input_dict['collection_mode'] + if self.mode == 'manual': + self.collection_layout = self.input_dict['collection_layout'].values + self.L = self.collection_layout[:, :2] # location of nodes [m] + self.A = self.collection_layout[:, 2:] # adjacency matrix for collection system. Zeroth element is substation + dim = self.A.shape + self.n_segments = dim[1]-1 # #turbines = # cable segments = # nodes - 1 + self.output_dict['total_turb'] = self.n_segments + self.C = np.zeros(self.n_segments + 1) # init capacity vector: cable ampacity needed at each turbine + self.calc_current_properties() + + def calc_current_properties(self): + """ + Find collection system voltage [V] and turbine amperage [A]. Sort cables by current capacity. + + Returns + ------- + self.collection_V: collection system voltage [V] + self.turbine_ampacity: turbine amperage [A] at collection system voltage + """ + self._total_turbine_counter = 0 + self.turbines_on_cable = [] + self.check_terminal = 0 + self.collection_V = 9999 + + for cable, property in self.input_dict['cable_specs_pd'].head().iterrows(): + if property['Rated Voltage (V)'] < self.collection_V: + self.collection_V = property['Rated Voltage (V)'] + + # sort cables by current capacity + self.input_dict['cable_specs_pd'].sort_values(by=['Current Capacity (A)'], inplace=True) + self.input_dict['cable_specs_pd'] = self.input_dict['cable_specs_pd'].reset_index(drop=True) + self.turbine_ampacity = self.input_dict['turbine_rating_MW']*1e3/self.collection_V + + def calc_required_segment_ampacity(self): + """ + Find the type of cable needed for each cable segment based on ampacity needed + Cable lengths are in meters in this method + The code starts with outermost nodes, and adds them to their receiver downstream (receiver) nodes. + This process continues until the substation is reached and all nodes have been assigned an ampacity. + Giver nodes are the parent nodes that are currently being considered. + Closed nodes are those that have contributed to all of their child nodes. + Nodes go from []->receiver->giver->closed + """ + # find outermost nodes + for i in range(1, self.n_segments + 1): + if self.A[i, :].sum() == 1: + self.C[i] = 1 + + giver = np.where(self.C > 0)[0] # nodes that give ampacity to downstream nodes + closed = np.empty(shape=(0, 0)) # nodes that have contributed to all their receiver nodes + while np.prod(self.C[1:]) == 0: # run until all turbine nodes are assigned a capacity + x = np.linspace(1, self.n_segments, self.n_segments, dtype='int64') + y = np.union1d(closed, giver) + for i in np.setdiff1d(x, y): # iterate through all nongiver, nonclosed nodes + receiver = np.where(self.A[i, :] == 1)[0] # nodes receiver to node i + a = len(np.intersect1d(receiver, giver)) + b = (len(receiver) - 1) + if a == b: # if all but one receiver node are givers + self.C[i] += sum(self.C[receiver]) + 1 # add giver node capacities to current node. +1 for this node's turbine + closed = np.append(closed, np.intersect1d(receiver, giver)) # giver nodes are now closed + giver = np.append(giver, i) # add node i to giver nodes + giver = np.setdiff1d(giver, closed) # removed closed nodes from giver + + self.C[0] = self.n_segments # substation handles all turbines + self.C *= self.turbine_ampacity # scale ampacity by turbine current + + """ + Create cable dictionary with cable start point, end point, length, ampacity, and bool representing if the cable segment is the terminal + """ + k = 0 # cable# iterator + remains = np.ones(self.n_segments + 1) # turbines still to have cables defined around + array_dict = dict() + # keys = {'Start point', 'End point', 'Length', 'Capacity'} + for i in range(0, self.n_segments): + for j in np.where(self.A[i, :] * remains == 1)[0]: + array_dict['cable' + str(k)] = dict() + array_dict['cable' + str(k)]['Start point'] = self.L[i, :] + array_dict['cable' + str(k)]['End point'] = self.L[j, :] + array_dict['cable' + str(k)]['Length'] = ((self.L[i, 0] - self.L[j, 0]) ** 2 + ( + self.L[i, 1] - self.L[j, 1]) ** 2) ** (1 / 2) + array_dict['cable' + str(k)]['Ampacity'] = min(self.C[i], self.C[j]) + array_dict['cable' + str(k)]['Terminal?'] = False + k += 1 # iterate to make new cable + remains[i] = False # prevent duplicate cables + # add terminal cable + array_dict['cable' + str(k)] = dict() + array_dict['cable' + str(k)]['Length'] = self.input_dict['distance_to_grid_connection_mi'] * 5280 * self._km_to_LF + array_dict['cable' + str(k)]['Ampacity'] = self.n_segments*self.turbine_ampacity + array_dict['cable' + str(k)]['Terminal?'] = True + self.array_dict = array_dict + def calc_num_strings(self): """ Calculate number of full and partial strings to support full plant @@ -337,7 +413,6 @@ def calc_num_strings(self): # # This means that self.output_dict['turb_per_partial_string'] cannot # be used an output value for the details output. - if self.output_dict['num_leftover_turb'] > 0: self.output_dict['num_partial_strings'] = 1 self.output_dict['perc_partial_string'] = self.calc_num_turb_partial_strings(self.output_dict['num_leftover_turb'], self.output_dict['num_turb_per_cable']) @@ -346,7 +421,6 @@ def calc_num_strings(self): self.output_dict['perc_partial_string'] = np.zeros(len(self.output_dict['num_turb_per_cable'])) # todo: output number of partial strings - return (self.output_dict['total_turb_per_string'], self.output_dict['num_full_strings'], self.output_dict['num_partial_strings'], self.output_dict['perc_partial_string'], self.output_dict['num_turb_per_cable']) @@ -381,6 +455,7 @@ def calc_num_turb_partial_strings(self, num_leftover_turb, num_turb_per_cable): turb_per_partial_string.append(0.0) num_remaining -= max_turb + # Calculate the percentage of full string turbines on a partial string perc_partial_string = np.divide(turb_per_partial_string, num_turb_per_cable) # Check to make sure there aren't any zeros in num_turbines_per_cable, which is used as the denominator @@ -395,12 +470,9 @@ def calc_num_turb_partial_strings(self, num_leftover_turb, num_turb_per_cable): perc_partial_string = np.nan_to_num(perc_partial_string) self.output_dict['turb_per_partial_string'] = turb_per_partial_string - return perc_partial_string - #TODO: change length_to_substation calculation as a user defined input? - @staticmethod - def calc_cable_len_to_substation(turbine_spacing_rotor_diameters, row_spacing_rotor_diameters, + def calc_cable_len_to_substation(self, turbine_spacing_rotor_diameters, row_spacing_rotor_diameters, num_strings): """ Calculate the distance for the largest cable run to substation @@ -447,12 +519,13 @@ def calc_cable_len_to_substation(turbine_spacing_rotor_diameters, row_spacing_ro turbine_spacing_rotor_diameters) ** 2)) # Sum up total length to substation - len_to_substation = np.sum(string_to_substation_length) + len_to_substation = np.sum(string_to_substation_length) * self.input_dict['rotor_diameter_m']/1000 # [km] return len_to_substation # todo: add to output csv @staticmethod - def calc_total_cable_length(cable, cable_specs, num_full_strings, num_partial_strings, len_to_substation, perc_partial_string): + def calc_total_cable_length(total_turbines, count, check_terminal, turbines_per_cable, cable, cable_specs, + num_full_strings, num_partial_strings, len_to_substation, perc_partial_string): """ Calculate total length of each cable type, km @@ -479,17 +552,64 @@ def calc_total_cable_length(cable, cable_specs, num_full_strings, num_partial_st Total length of individual cable type """ - if cable.turb_sequence == len(cable_specs): - # Only add len_to_substation to the final cable in the string - total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + len_to_substation) - else: - total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string)) + # if cable.turb_sequence == len(cable_specs): + # # Only add len_to_substation to the final cable in the string + # total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + len_to_substation) + # else: + # total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string)) + # return total_cable_len # todo: add to output csv + + # If terminal cable has already been accounted for, skip any calculations for other cables. + if (cable.turb_sequence - 1) > check_terminal: + cable.array_cable_len = 0 + cable.total_length = 0 + cable.num_turb_per_cable = 0 + return 0, 0 - return total_cable_len # todo: add to output csv + # If num full strings < = 1, find which cable the final turbine is on, and calculate total cable length + # (including the len to substation) using that cable. + if num_full_strings <= 1 and num_partial_strings >= 0: # Essentially a switch for distributed wind - def create_ArraySystem(self): + # if number of turbines is less than total string capacity, find the terminal cable and find total cable len + # up till that cable. + if total_turbines <= turbines_per_cable[count]: # If total turbines in project are less than cumulative turbines uptil and including that cable. + # total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + len_to_substation) + terminal_string = cable.turb_sequence - 1 # Flag the cable that is the actual terminal cable + + if (cable.turb_sequence - 1 ) == 0: # That is, if cable #1 can hold more turbines than specified by user + + cable.num_turb_per_cable = total_turbines + cable.array_cable_len = ( + (cable.num_turb_per_cable + cable.downstream_connection) * cable.turb_section_length) + + total_cable_len = ((num_full_strings * cable.array_cable_len) + (num_partial_strings * cable.array_cable_len)) + len_to_substation + + else: + cable.num_turb_per_cable = total_turbines - turbines_per_cable[(count - 1)] + cable.array_cable_len = ( + (cable.num_turb_per_cable + cable.downstream_connection) * cable.turb_section_length) + + total_cable_len = ((num_full_strings * cable.array_cable_len) + + (num_partial_strings * cable.array_cable_len)) + len_to_substation + + return total_cable_len, terminal_string + + else: + total_cable_len = num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + + else: # Switch for utility scale landbosse + if cable.turb_sequence == len(cable_specs): + # Only add len_to_substation to the final cable in the string + total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + len_to_substation) + else: + total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string)) + + + return total_cable_len, 9999 + + def create_ArraySystem(self): #data used in parent classes: self.addl_specs = dict() self.addl_specs['turbine_rating_MW'] = self.input_dict['turbine_rating_MW'] @@ -498,10 +618,8 @@ def create_ArraySystem(self): self.addl_specs['turbine_spacing_rotor_diameters'] = self.input_dict['turbine_spacing_rotor_diameters'] self.addl_specs['rotor_diameter_m'] = self.input_dict['rotor_diameter_m'] self.addl_specs['line_frequency_hz'] = self.input_dict['line_frequency_hz'] - - - - + self.addl_specs['mode'] = self.mode + self.addl_specs['turbine_ampacity'] = self.turbine_ampacity system = { 'upstream_turb': self.addl_specs['upstream_turb'], 'turb_sequence': self.addl_specs['turb_sequence'], @@ -512,8 +630,6 @@ def create_ArraySystem(self): # Loops through all user defined array cable types, composing them # in ArraySystem - # TODO: Sort input cable types by ascending current capacity - self.cables = {} self.input_dict['cable_specs'] = self.input_dict['cable_specs_pd'].T.to_dict() n=0 #to keep tab of number of cables input by user. @@ -540,31 +656,102 @@ def create_ArraySystem(self): self.output_dict['num_strings'] = self.output_dict['num_full_strings'] + self.output_dict['num_partial_strings'] - if self.input_dict['user_defined_home_run_trench'] == 0: + if self.input_dict['user_defined_home_run_trench'] == 0: + distributed_wind_distance_to_grid = (self.input_dict['turbine_spacing_rotor_diameters'] * self.input_dict['rotor_diameter_m']) / 1000 #This only gets used if number of strings is <= 1 self.output_dict['trench_len_to_substation_km'] = self.calc_cable_len_to_substation(self.input_dict['turbine_spacing_rotor_diameters'], self.input_dict['row_spacing_rotor_diameters'], self.output_dict['num_strings']) else: self.output_dict['trench_len_to_substation_km'] = self.input_dict['trench_len_to_substation_km'] self.output_dict['cable_len_to_substation_km'] = self.output_dict['trench_len_to_substation_km'] # assumes 3 conductors and fiber and neutral + cable_sequence = 0 + # Make a list of how many turbines per cable + for _, (name, cable) in enumerate(self.cables.items()): + if cable_sequence == 0: + self.turbines_on_cable.append(cable.num_turb_per_cable) + else: + self.turbines_on_cable.append(cable.num_turb_per_cable + self.turbines_on_cable[(cable_sequence - 1)]) + # turbines_on_cable[cable_sequence] += cable.num_turb_per_cable + cable_sequence += 1 + self.__turbines_on_cable = self.turbines_on_cable + # Calculate total length of each cable type, and total cost that calculated length of cable: for idx, (name, cable) in enumerate(self.cables.items()): - cable_specs = self.input_dict['cable_specs'] - num_full_strings = self.output_dict['num_full_strings'] - num_partial_strings = self.output_dict['num_partial_strings'] - trench_len_to_substation_km = self.output_dict['trench_len_to_substation_km'] - perc_partial_string = self.output_dict['perc_partial_string'][idx] - total_cable_len = self.calc_total_cable_length(cable, cable_specs, num_full_strings, num_partial_strings, - trench_len_to_substation_km, perc_partial_string) - - self._cable_length_km[name] = total_cable_len + # cable_specs = self.input_dict['cable_specs'] + # num_full_strings = self.output_dict['num_full_strings'] + # num_partial_strings = self.output_dict['num_partial_strings'] + # trench_len_to_substation_km = self.output_dict['trench_len_to_substation_km'] + # perc_partial_string = self.output_dict['perc_partial_string'][idx] + pps = self.output_dict['perc_partial_string'][idx] + total_cable_len, self.check_terminal = self.calc_total_cable_length(self.output_dict['total_turb'], idx, + self.check_terminal, self.__turbines_on_cable, + cable, self.input_dict['cable_specs'], + self.output_dict['num_full_strings'], + self.output_dict['num_partial_strings'], + self.output_dict['cable_len_to_substation_km'], + pps, + ) + + self._cable_length_km[name] = total_cable_len #self.__cable_cost_usd[name] = cable.__dict__['cost'] - cable.total_length = total_cable_len self.output_dict['total_cable_len_km'] += total_cable_len # cable.total_mass = total_cable_len * cable.mass - cable.total_cost = (total_cable_len / self._km_to_LF)* cable.cost - self._total_cable_cost+=cable.total_cost #Keep running tally of total cable cost used in wind farm. + cable.total_cost = (total_cable_len / self._km_to_LF) * cable.cost + self._total_cable_cost += cable.total_cost # Keep running tally of total cable cost used in wind farm. + + def create_manual_ArraySystem(self): + + # data used in parent classes: + self.addl_specs = dict() + self.addl_specs['turbine_rating_MW'] = self.input_dict['turbine_rating_MW'] + self.addl_specs['depth'] = self.input_dict['depth'] + self.addl_specs['line_frequency_hz'] = self.input_dict['line_frequency_hz'] + self.addl_specs['mode'] = self.mode + + # calculate cable segment requirements + self.calc_required_segment_ampacity() + + # Loops through all user defined cable types, composing them + # in ArraySystem + + self.cables = {} + self.input_dict['cable_specs'] = self.input_dict['cable_specs_pd'].T.to_dict() + n = 0 # to keep tab of number of cables input by user. + while n < len(self.input_dict['cable_specs']): + specs = self.input_dict['cable_specs'][n] + # Create instance of each cable and assign to ArraySystem.cables + cable = Cable(specs, self.addl_specs) + n += 1 + + # self.cables[name] stores value which is a new instantiation of object of type Cable. + self.cables[specs['Array Cable']] = cable + self.output_dict['cables'] = self.cables + + # Calculate total length and cost of each cable type + # Calculate total cable cost and power dissipated in the collection system: + dissipated_power = 0 #W + for segment in self.array_dict: + for idx, (name, cable) in enumerate(self.cables.items()): + if cable.current_capacity >= self.array_dict[segment]['Ampacity']: + cable.total_length += self.array_dict[segment]['Length'] + self.output_dict['total_cable_len_km'] += self.array_dict[segment]['Length'] + # cable.total_mass = cable.total_length * cable.mass + cable.total_cost = (cable.total_length / self._km_to_LF) * cable.cost + self._total_cable_cost += (self.array_dict[segment]['Length'] / self._km_to_LF) * cable.cost # Keep running tally of total cable cost used in wind farm. + dissipated_power += 3 * self.array_dict[segment]['Ampacity']**2 * abs(cable.ac_resistance)*self.array_dict[segment]['Length']/1000 #TODO P=3*I^2*R. IF 3 phase. Divide by 1000 to go from ohm/km->ohm/m + break # only assign one cable to each segment + + # add substation to transmission interconnect cable + terminal_ampacity = self.n_segments * self.turbine_ampacity + + self.output_dict['dissipated_power'] = dissipated_power + # print('TOTAL CABLE COST = $' + str(self._total_cable_cost)) # TODO remove after figuring out layout optimizer + # print('Dissipated power [W] = ' + str(dissipated_power)) #TODO remove after layout optimizer figured out + self.output_dict['total_cable_cost'] = self._total_cable_cost + self.output_dict['distance_to_grid_connection_km'] = self.input_dict['distance_to_grid_connection_mi']*5280*self._km_to_LF + + self.output_dict['cable_len_to_grid_connection_km'] = self.output_dict['distance_to_grid_connection_km'] def calculate_trench_properties(self, trench_properties_input, trench_properties_output): """ @@ -617,7 +804,6 @@ def estimate_construction_time(self, construction_time_input_data, construction_ trench_length_km = construction_time_output_data['trench_length_km'] operation_data = throughput_operations.where(throughput_operations['Module'] == 'Collection').dropna(thresh=4) # operation_data = pd.merge() - # from rsmeans data, only read in Collection related data and filter out the rest: cable_trenching = throughput_operations[throughput_operations.Module == 'Collection'] @@ -628,7 +814,6 @@ def estimate_construction_time(self, construction_time_input_data, construction_ construction_time_output_data['trenching_labor_usd_per_hr']=trenching_labor_usd_per_hr trenching_labor_daily_output = trenching_labor['Daily output'].values[0] # Units: LF/day -> where LF = Linear Foot trenching_labor_num_workers = trenching_labor['Number of workers'].sum() - # Storing data with equipment related inputs: trenching_equipment = cable_trenching[cable_trenching.values == 'Equipment'] trenching_cable_equipment_usd_per_hr = trenching_equipment['Rate USD per unit'].sum() @@ -636,13 +821,11 @@ def estimate_construction_time(self, construction_time_input_data, construction_ trenching_equipment_daily_output = trenching_equipment['Daily output'].values[0] # Units: LF/day -> where LF = Linear Foot construction_time_output_data['trenching_labor_daily_output'] = trenching_labor_daily_output construction_time_output_data['trenching_equipment_daily_output'] = trenching_equipment_daily_output - operation_data['Number of days taken by single crew'] = ((trench_length_km / self._km_to_LF) / trenching_labor_daily_output) operation_data['Number of crews'] = np.ceil((operation_data['Number of days taken by single crew'] / 30) / collection_construction_time) operation_data['Cost USD without weather delays'] = ((trench_length_km / self._km_to_LF) / trenching_labor_daily_output) * (operation_data['Rate USD per unit'] * construction_time_input_data['operational_hrs_per_day']) alpha = operation_data[operation_data['Type of cost'] == 'Collection'] operation_data_id_days_crews_workers = alpha[['Operation ID', 'Number of days taken by single crew', 'Number of crews', 'Number of workers']] - alpha = operation_data[operation_data['Type of cost'] == 'Labor'] operation_data_id_days_crews_workers = alpha[['Operation ID', 'Number of days taken by single crew', 'Number of crews', 'Number of workers']] @@ -670,8 +853,6 @@ def estimate_construction_time(self, construction_time_input_data, construction_ return construction_time_output_data['operation_data_entire_farm'] - - def calculate_costs(self, calculate_costs_input_dict, calculate_costs_output_dict): #read in rsmeans data: @@ -718,11 +899,18 @@ def calculate_costs(self, calculate_costs_input_dict, calculate_costs_output_dic collection_cost = collection_cost.append(cable_cost_usd_per_LF_df) # Calculate Mobilization Cost and add to collection_cost dataframe: - mobilization_cost = pd.DataFrame([['Mobilization', collection_cost['Cost USD'].sum() * 0.05 , 'Collection']], + # For utility scale plants, mobilization is assumed to be 5% of the sum of labor, equipment, and material costs. + # For distributed mode, mobilization is a calculated % that is a function of turbine size. + if calculate_costs_input_dict['num_turbines'] > 10: + calculate_costs_output_dict['mob_cost'] = collection_cost['Cost USD'].sum() * 0.05 + else: + calculate_costs_output_dict['mob_cost'] = (collection_cost['Cost USD'].sum() / calculate_costs_input_dict['num_turbines']) * self.mobilization_cost(calculate_costs_input_dict['turbine_rating_MW']) + mobilization_cost = pd.DataFrame([['Mobilization', calculate_costs_output_dict['mob_cost'], 'Collection']], columns=['Type of cost', 'Cost USD', 'Phase of construction']) collection_cost = collection_cost.append(mobilization_cost) calculate_costs_output_dict['total_collection_cost'] = collection_cost + calculate_costs_output_dict['sum_collection_cost'] = collection_cost['Cost USD'].sum() return collection_cost @@ -744,7 +932,7 @@ def outputs_for_detailed_tab(self, input_dict, output_dict): 'variable_df_key_col_name': 'Total Number of Turbines', 'value': float(self.output_dict['total_turb']) }) - + result.append({ 'unit': 'km', 'type': 'variable', @@ -758,49 +946,50 @@ def outputs_for_detailed_tab(self, input_dict, output_dict): 'variable_df_key_col_name': 'Total cable length', 'value': float(self.output_dict['total_cable_len_km']) }) - - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Number of Turbines Per String in Full String', - 'value': float(self.output_dict['total_turb_per_string']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Number of Full Strings', - 'value': float(self.output_dict['num_full_strings']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Number of Turbines in Partial String', - 'value': float(self.output_dict['num_leftover_turb']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Number of Partial Strings', - 'value': float(self.output_dict['num_partial_strings']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Total number of strings full + partial', - 'value': float(self.output_dict['num_full_strings'] + self.output_dict['num_partial_strings']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Trench Length to Substation (km)', - 'value': float(self.output_dict['trench_len_to_substation_km']) - }) - result.append({ - 'unit': '', - 'type': 'variable', - 'variable_df_key_col_name': 'Cable Length to Substation (km)', - 'value': float(self.output_dict['cable_len_to_substation_km']) - }) + + if self.mode == 'auto': + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Turbines Per String in Full String', + 'value': float(self.output_dict['total_turb_per_string']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Full Strings', + 'value': float(self.output_dict['num_full_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Turbines in Partial String', + 'value': float(self.output_dict['num_leftover_turb']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Partial Strings', + 'value': float(self.output_dict['num_partial_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Total number of strings full + partial', + 'value': float(self.output_dict['num_full_strings'] + self.output_dict['num_partial_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Trench Length to Substation (km)', + 'value': float(self.output_dict['trench_len_to_substation_km']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Cable Length to Substation (km)', + 'value': float(self.output_dict['cable_len_to_substation_km']) + }) cables = '' n = 1 # to keep tab of number of cables input by user. @@ -835,33 +1024,19 @@ def outputs_for_detailed_tab(self, input_dict, output_dict): }) n += 1 - result.append({ - 'unit': '', - 'type': 'list', - 'variable_df_key_col_name': 'Number of turbines per cable type in full strings [' + cables + ']', - - 'value': str(self.output_dict['num_turb_per_cable']) - }) - - # self.output_dict['turb_per_partial_string'] is only available if - # self.output_dict['num_leftover_turb'] > 0 which is not always the - # case. Commenting this output out - - # result.append({ - # 'unit': '', - # 'type': 'list', - # 'variable_df_key_col_name': 'Number of turbines per cable type in partial string [' + cables + ']', - # - # 'value': str(self.output_dict['turb_per_partial_string']) - # }) - - result.append({ - 'unit': '', - 'type': 'list', - 'variable_df_key_col_name': 'Percent length of cable in partial string [' + cables + ']', - - 'value': str(self.output_dict['perc_partial_string']) - }) + if self.mode == 'auto': + result.append({ + 'unit': '', + 'type': 'list', + 'variable_df_key_col_name': 'Percent length of cable in partial string [' + cables + ']', + 'value': str(self.output_dict['perc_partial_string']) + }) + result.append({ + 'unit': '', + 'type': 'list', + 'variable_df_key_col_name': 'Number of turbines per cable type in full strings [' + cables + ']', + 'value': str(self.output_dict['num_turb_per_cable']) + }) for row in self.output_dict['management_crew'].itertuples(): dashed_row = ' <--> '.join(str(x) for x in list(row)) @@ -895,9 +1070,14 @@ def run_module(self): Runs the CollectionCost module and populates the IO dictionaries with calculated values. """ + operational_hrs_per_day = self.input_dict['hour_day'][self.input_dict['time_construct']] + self.input_dict['operational_hrs_per_day'] = operational_hrs_per_day try: - self.create_ArraySystem() + if self.mode == 'auto': + self.create_ArraySystem() + elif self.mode == 'manual': + self.create_manual_ArraySystem() self.calculate_trench_properties(self.input_dict, self.output_dict) operation_data = self.estimate_construction_time(self.input_dict, self.output_dict) diff --git a/landbosse/model/CollectionCostOriginal.py b/landbosse/model/CollectionCostOriginal.py new file mode 100644 index 00000000..3b891d2b --- /dev/null +++ b/landbosse/model/CollectionCostOriginal.py @@ -0,0 +1,934 @@ +""" +**CollectionCost.py** +- Created by Matt Shields for Offshore BOS +- Refactored by Parangat Bhaskar for LandBOSSE + +NREL - 05/31/2019 + +This module consists of two classes: + +- The first class in this module is the parent class Cable, with a sublass Array that inherits from Cable + +- The second class is the ArraySystem class that instantiates the Array class and determines the wind farm layout and calculates total collection system cost +""" + +import math +import numpy as np +import traceback +import pandas as pd + +from .CostModule import CostModule +from .WeatherDelay import WeatherDelay as WD + + +class Cable: + """ + + Create an instance of Cable (either array or export) + + Parameters + --------- + cable_specs : dict + Dictionary containing cable specifications + line_frequency_hz : int + Additional user inputs + + Returns + ------- + current_capacity : float + Cable current rating at 1m burial depth, Amps + rated_voltage : float + Cable rated voltage, kV + ac_resistance : float + Cable resistance for AC current, Ohms/km + inductance : float + Cable inductance, mH/km + capacitance : float + Cable capacitance, nF/km + cost : int + Cable cost, $US/km + char_impedance : float + Characteristic impedance of equivalent cable circuit, Ohms + power_factor : float + Power factor of AC current in cable (nondim) + cable_power : float + Maximum 3-phase power dissipated in cable, MW + + """ + + def __init__(self, cable_specs, addl_specs): + """ + Parameters + ---------- + cable_specs : dict + The input dictionary with key value pairs described in the + class documentation + + addl_specs : dict + The output dictionary with key value pairs as found on the + output documentation. + + """ + + self.current_capacity = cable_specs['Current Capacity (A)'] + self.rated_voltage = cable_specs['Rated Voltage (V)'] + self.ac_resistance = cable_specs['AC Resistance (Ohms/km)'] + self.inductance = cable_specs['Inductance (mH/km)'] + self.capacitance = cable_specs['Capacitance (nF/km)'] + self.cost = cable_specs['Cost (USD/LF)'] + self.line_frequency_hz = addl_specs['line_frequency_hz'] + + + # Calc additional cable specs + self.calc_char_impedance(self.line_frequency_hz) + self.calc_power_factor() + self.calc_cable_power() + + def calc_char_impedance(self, line_frequency_hz): + """ + Calculate characteristic impedance of cable, Ohms + + Parameters + ---------- + line_frequency_hz : int + Frequency of AC current, Hz + """ + conductance = 1 / self.ac_resistance + + num = complex(self.ac_resistance, 2 * math.pi * line_frequency_hz * self.inductance) + den = complex(conductance, 2 * math.pi * line_frequency_hz * self.capacitance) + self.char_impedance = np.sqrt(num / den) + + def calc_power_factor(self): + """ + Calculate power factor + """ + + phase_angle = math.atan(np.imag(self.char_impedance) / + np.real(self.char_impedance)) + self.power_factor = math.cos(phase_angle) + + def calc_cable_power(self): + """ + Calculate maximum power transfer through 3-phase cable, MW + """ + + # TODO: Verify eqn is correct + self.cable_power = (np.sqrt(3) * self.rated_voltage * self.current_capacity * self.power_factor / 1000) + + +class Array(Cable): + """Array cable base class""" + + def __init__(self, cable_specs, addl_inputs): + """ + Creates an instance of Array cable. + (May be multiple instances of different capacity cables in a string) + + Parameters + ---------- + cable_specs : dict + Dictionary containing following cable specifications: + + - turbine_rating_MW + + - upstream_turb + + - turbine_spacing_rotor_diameters + + - rotor_diameter_m + + addl_inputs : dict + + - Any additional user inputs + + Returns + ------- + self.max_turb_per_cable : float + Maximum number of turbines (at turbine_rating_MW) an individual cable + can support + self.num_turb_per_cable : float + Number of turbines each cable in a string actually supports. + self.turb_sequence : float + Ordering of cable in string, starting with smallest cable at 0 + self.downstream_connection : int + Additional cable length requried to connect between different sized + cables (for first cable in string only) + self.array_cable_len : float + Length of individual cable in a string, km + """ + + super().__init__(cable_specs, addl_inputs) + self.line_frequency_hz = addl_inputs['line_frequency_hz'] + self.calc_max_turb_per_cable(addl_inputs) + self.calc_num_turb_per_cable(addl_inputs) + self.calc_array_cable_len(addl_inputs) + + def calc_max_turb_per_cable(self, addl_inputs): + """ + Calculate the number of turbines that each cable can support + + Parameters + ---------- + turbine_rating_MW : int + Nameplate capacity of individual turbines + """ + + turbine_rating_MW = addl_inputs['turbine_rating_MW'] + + self.max_turb_per_cable = np.floor(self.cable_power / turbine_rating_MW) + + def calc_num_turb_per_cable(self, addl_inputs): + """ + Calculates actual number of turbines per cable, accounting for upstream + turbines. + + Parameters + ---------- + upstream_turb : int + Number of turbines on upstream cables in string + """ + + upstream_turb = addl_inputs['upstream_turb'] + self.turb_sequence = addl_inputs['turb_sequence'] + + self.num_turb_per_cable = self.max_turb_per_cable - upstream_turb # todo: add to ouptut csv + + if upstream_turb == 0: + self.downstream_connection = -1 + else: + self.downstream_connection = 0 + + def calc_array_cable_len(self, addl_inputs): + """ + Calculate array cable length per string, km + + Parameters + ---------- + turbine_spacing_rotor_diameters : int + Spacing between turbines in string, # of rotor diameters + rotor_diameter_m : int or float + Rotor diameter, m + """ + + turbine_spacing_rotor_diameters = addl_inputs['turbine_spacing_rotor_diameters'] + rotor_diameter_m = addl_inputs['rotor_diameter_m'] + + self.calc_turb_section_len(turbine_spacing_rotor_diameters, rotor_diameter_m) + + self.array_cable_len = ((self.num_turb_per_cable + self.downstream_connection) * self.turb_section_length) # todo: add to output csv + + # @staticmethod + def calc_turb_section_len(self, turbine_spacing_rotor_diameters, rotor_diameter_m): + """ + Calculate array cable section length between two turbines. Also, section length == trench length. Which means + trench_length = cable_length for that section. + + Parameters + ---------- + turbine_spacing_rotor_diameters : int + Spacing between turbines in string, # of rotor diameters + rotor_diameter_m : int or float + Rotor diameter, m + + Returns + ------- + turb_connect_len : int + Length of array cable between two turbines, km + """ + + self.turb_section_length = (turbine_spacing_rotor_diameters * rotor_diameter_m) / 1000 + + return self.turb_section_length + + +class ArraySystem(CostModule): + """ + + + \nThis module: + + * Calculates cable length to substation + + * Calculates number of strings in a subarray + + * Calculated number of strings + + * Calculates total cable length for each cable type + + * Calculates total trench length + + * Calculates total collection system cost based on amount of material, amount of labor, price data, cable length, and trench length. + + + + **Keys in the input dictionary are the following:** + + * Given below are attributes that define each cable type: + * conductor_size + (int) cross-sectional diameter of cable [in mm] + + + + """ + + def __init__(self, input_dict, output_dict, project_name): + + self.input_dict = input_dict + self.output_dict = output_dict + self.project_name = project_name + self.output_dict['total_cable_len_km'] = 0 + self._km_to_LF = 0.0003048 #Units: [km/LF] Conversion factor for converting from km to linear foot. + self._total_cable_cost = 0 + + self._cable_length_km = dict() + + + def calc_num_strings(self): + """ + Calculate number of full and partial strings to support full plant + capacity. + + Parameters + ---------- + available cables : dict + Dictionary of cable types + plant_capacity : int | float + Total capcity of wind plant (MW) + turbine_capacity : int | float + Nameplate capacity of individual turbines (MW) + + Returns + ------- + self.output_dict['total_turb_per_string'] : float + Number of turbines on each string + self.output_dict['num_full_strings'] : float + Number of complete strings in array + turb_per_partial_string : float + Number of turbines in the partial string (if applicable) + self.output_dict['num_partial_strings'] : float + Number of partial strings (if applicable, 0 or 1) + perc_full_string : list + Percentage of maximum number of turbines per cable type on + partial string + self.output_dict['num_turb_per_cable'] : list + Number of turbines on each cable type in string + """ + + # Calculate total number of individual turbines in wind plant + self.output_dict['total_turb'] = self.input_dict['num_turbines'] + + # Calculate the number of turbines on each cable type in a string + self.output_dict['num_turb_per_cable'] = [cable.num_turb_per_cable for cable in self.cables.values()] + + # Calculate the total number of turbines per string + self.output_dict['total_turb_per_string'] = sum(self.output_dict['num_turb_per_cable']) + + # Calculate number of full strings and any remainder required to + # support the total number of turbines + self.output_dict['num_full_strings'] = np.floor(self.output_dict['total_turb'] / self.output_dict['total_turb_per_string']) # todo: add to output csv + self.output_dict['num_leftover_turb'] = self.output_dict['total_turb'] % self.output_dict['total_turb_per_string'] + + # Calculate number of turbines on a remaining partial string + + # Note: self.output_dict['turb_per_partial_string'] is only set if + # calc_num_turb_partial_strings() + # is called, which isn't always the case, as seen in the if...else construct below + # + # This means that self.output_dict['turb_per_partial_string'] cannot + # be used an output value for the details output. + + if self.output_dict['num_leftover_turb'] > 0: + self.output_dict['num_partial_strings'] = 1 + self.output_dict['perc_partial_string'] = self.calc_num_turb_partial_strings(self.output_dict['num_leftover_turb'], self.output_dict['num_turb_per_cable']) + else: + self.output_dict['num_partial_strings'] = 0 + self.output_dict['perc_partial_string'] = np.zeros(len(self.output_dict['num_turb_per_cable'])) + + # todo: output number of partial strings + + return (self.output_dict['total_turb_per_string'], self.output_dict['num_full_strings'], self.output_dict['num_partial_strings'], + self.output_dict['perc_partial_string'], self.output_dict['num_turb_per_cable']) + + def calc_num_turb_partial_strings(self, num_leftover_turb, num_turb_per_cable): + """ + If a partial string exists, calculate the percentage of turbines on + each cable relative to a full string + + Parameters + ---------- + self.output_dict['num_leftover_turb'] : float + Number of turbines in partial string + self.output_dict['num_turb_per_cable'] : list + List of number of turbines per cable type on a full string + + Returns + ------- + np.array + Array of percent of turbines per cable type on partial string + relative to full string + """ + + num_remaining = num_leftover_turb + turb_per_partial_string = [] + + # Loop through each cable type in the string. Determine how many + # turbines are required for each cable type on the partial string + for max_turb in num_turb_per_cable: + if num_remaining > 0: + turb_per_partial_string.append(min(num_remaining, max_turb)) + else: + turb_per_partial_string.append(0.0) + num_remaining -= max_turb + + perc_partial_string = np.divide(turb_per_partial_string, num_turb_per_cable) + + # Check to make sure there aren't any zeros in num_turbines_per_cable, which is used as the denominator + # in the division above (this happens when not all of the cable types in the input sheet need to be used). + # If there is a zero, then print a warning and change NaN to 0 in perc_partial_string. + if 0.0 in num_turb_per_cable: + print( + f'Warning: {self.project_name} CollectionCost module generates number of turbines per string that ' + f'includes a zero entry. Please confirm that there not all cable types need to be used for the number of turbines that are being run.' + f' num_turbines={self.input_dict["num_turbines"]} rating_MW={self.input_dict["turbine_rating_MW"]}' + f' num_turb_per_cable: {num_turb_per_cable}') + perc_partial_string = np.nan_to_num(perc_partial_string) + + self.output_dict['turb_per_partial_string'] = turb_per_partial_string + + return perc_partial_string + + #TODO: change length_to_substation calculation as a user defined input? + @staticmethod + def calc_cable_len_to_substation(turbine_spacing_rotor_diameters, row_spacing_rotor_diameters, + num_strings): + """ + Calculate the distance for the largest cable run to substation + Assumes substation is in the center of the layout, 1 row spacing in + front of first row + + Parameters + ---------- + turbine_spacing_rotor_diameters : int or float + Spacing between turbines in a row, # of rotor diameters + row_spacing_rotor_diameters : int or float + Spacing between rows in wind plant, # of rotor diameters + num_strings : int + Total number of strings + + Returns + ------- + len_to_substation : int or float + Total length of largest array cable required to connect each string + to substation, km + """ + + # Define spacing terms for even or odd number of strings + # Even number: substation centered between middle two strings + # Odd number : substation centered on middle string + if (num_strings % 2) == 0: + n_max = int(num_strings / 2) + turb_space_scaling = 0.5 + range_strings = range(1, n_max + 1) + else: + n_max = int((num_strings - 1) / 2) + turb_space_scaling = 1 + range_strings = range(n_max + 1) + + # Calculate hypotenuse length of each string to substation + string_to_substation_length = [] + for idx in range_strings: + if idx == 0: + c = 1 + else: + c = 2 + string_to_substation_length.append(c * np.sqrt(row_spacing_rotor_diameters ** 2 + + (turb_space_scaling * idx * + turbine_spacing_rotor_diameters) ** 2)) + + # Sum up total length to substation + len_to_substation = np.sum(string_to_substation_length) + + return len_to_substation # todo: add to output csv + + @staticmethod + def calc_total_cable_length(cable, cable_specs, num_full_strings, num_partial_strings, len_to_substation, perc_partial_string): + """ + Calculate total length of each cable type, km + + Parameters + ---------- + cable : object + Instance of individual cable type + cable_specs : dict + Dictionary containing cable specifications + self.output_dict['num_full_strings'] : float + Number of complete strings in array + self.output_dict['num_partial_strings'] : float + Number of partial strings (if applicable, 0 or 1) + len_to_substation : int or float + Total length of largest array cable required to connect each string + to substation, km + self.output_dict['perc_partial_string'] : list + List of percent of turbines per cable type on partial string + relative to full string + + Returns + ------- + total_cable_len : int or float + Total length of individual cable type + """ + + if cable.turb_sequence == len(cable_specs): + # Only add len_to_substation to the final cable in the string + total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string) + len_to_substation) + else: + total_cable_len = (num_full_strings * cable.array_cable_len + num_partial_strings * (cable.array_cable_len * perc_partial_string)) + + return total_cable_len # todo: add to output csv + + def create_ArraySystem(self): + + + #data used in parent classes: + self.addl_specs = dict() + self.addl_specs['turbine_rating_MW'] = self.input_dict['turbine_rating_MW'] + self.addl_specs['upstream_turb'] = 0 + self.addl_specs['turb_sequence'] = 1 + self.addl_specs['turbine_spacing_rotor_diameters'] = self.input_dict['turbine_spacing_rotor_diameters'] + self.addl_specs['rotor_diameter_m'] = self.input_dict['rotor_diameter_m'] + self.addl_specs['line_frequency_hz'] = self.input_dict['line_frequency_hz'] + + + + + system = { + 'upstream_turb': self.addl_specs['upstream_turb'], + 'turb_sequence': self.addl_specs['turb_sequence'], + 'turbine_rating_MW' : self.addl_specs['turbine_rating_MW'], + 'turbine_spacing_rotor_diameters': self.addl_specs['turbine_spacing_rotor_diameters'], + 'rotor_diameter_m': self.addl_specs['rotor_diameter_m'] + } + + # Loops through all user defined array cable types, composing them + # in ArraySystem + # TODO: Sort input cable types by ascending current capacity + + self.cables = {} + self.input_dict['cable_specs'] = self.input_dict['cable_specs_pd'].T.to_dict() + n=0 #to keep tab of number of cables input by user. + while n 4)] = 10 + weather_delay_output_data['wind_delay_time'] = float(wind_delay.sum()) + + return weather_delay_output_data + + def estimate_construction_time(self, construction_time_input_data, construction_time_output_data): + """ + Function to estimate construction time on per turbine basis. TODO: What's a better definition of this function. It's task is to return a pd.DataFrame (operation_data). + + Parameters + ------- + duration_construction + + pd.DataFrame + rsmeans + + pd.DataFrame + trench_length_km + + + + Returns + ------- + + (pd.DataFrame) operation_data + + """ + + collection_construction_time = construction_time_input_data['construct_duration'] * 1 / 3 # assumes collection construction occurs for one-third of project duration + + throughput_operations = construction_time_input_data['rsmeans'] + trench_length_km = construction_time_output_data['trench_length_km'] + operation_data = throughput_operations.where(throughput_operations['Module'] == 'Collection').dropna(thresh=4) + # operation_data = pd.merge() + + # from rsmeans data, only read in Collection related data and filter out the rest: + cable_trenching = throughput_operations[throughput_operations.Module == 'Collection'] + + # Storing data with labor related inputs: + trenching_labor = cable_trenching[cable_trenching.values == 'Labor'] + trenching_labor_usd_per_hr = trenching_labor['Rate USD per unit'].sum() + + construction_time_output_data['trenching_labor_usd_per_hr']=trenching_labor_usd_per_hr + trenching_labor_daily_output = trenching_labor['Daily output'].values[0] # Units: LF/day -> where LF = Linear Foot + trenching_labor_num_workers = trenching_labor['Number of workers'].sum() + + # Storing data with equipment related inputs: + trenching_equipment = cable_trenching[cable_trenching.values == 'Equipment'] + trenching_cable_equipment_usd_per_hr = trenching_equipment['Rate USD per unit'].sum() + construction_time_output_data['trenching_cable_equipment_usd_per_hr']=trenching_cable_equipment_usd_per_hr + trenching_equipment_daily_output = trenching_equipment['Daily output'].values[0] # Units: LF/day -> where LF = Linear Foot + construction_time_output_data['trenching_labor_daily_output'] = trenching_labor_daily_output + construction_time_output_data['trenching_equipment_daily_output'] = trenching_equipment_daily_output + + operation_data['Number of days taken by single crew'] = ((trench_length_km / self._km_to_LF) / trenching_labor_daily_output) + operation_data['Number of crews'] = np.ceil((operation_data['Number of days taken by single crew'] / 30) / collection_construction_time) + operation_data['Cost USD without weather delays'] = ((trench_length_km / self._km_to_LF) / trenching_labor_daily_output) * (operation_data['Rate USD per unit'] * construction_time_input_data['operational_hrs_per_day']) + alpha = operation_data[operation_data['Type of cost'] == 'Collection'] + operation_data_id_days_crews_workers = alpha[['Operation ID', 'Number of days taken by single crew', 'Number of crews', 'Number of workers']] + + alpha = operation_data[operation_data['Type of cost'] == 'Labor'] + operation_data_id_days_crews_workers = alpha[['Operation ID', 'Number of days taken by single crew', 'Number of crews', 'Number of workers']] + + # if more than one crew needed to complete within construction duration then assume that all construction + # happens within that window and use that timeframe for weather delays; + # if not, use the number of days calculated + operation_data['time_construct_bool'] = operation_data['Number of days taken by single crew'] > collection_construction_time * 30 + boolean_dictionary = {True: collection_construction_time * 30, False: np.NAN} + operation_data['time_construct_bool'] = operation_data['time_construct_bool'].map(boolean_dictionary) + operation_data['Time construct days'] = operation_data[['time_construct_bool', 'Number of days taken by single crew']].min(axis=1) + num_days = operation_data['Time construct days'] + + # pull out management data + crew_cost = self.input_dict['crew_cost'] + crew = self.input_dict['crew'][self.input_dict['crew']['Crew type ID'].str.contains('M0')] + management_crew = pd.merge(crew_cost, crew, on=['Labor type ID']) + management_crew = management_crew.assign(per_diem_total=management_crew['Per diem USD per day'] * management_crew['Number of workers'] * num_days.iloc[0]) + management_crew = management_crew.assign(hourly_costs_total=management_crew['Hourly rate USD per hour'] * self.input_dict['hour_day'][self.input_dict['time_construct']] * num_days.iloc[0]) + management_crew = management_crew.assign(total_crew_cost_before_wind_delay=management_crew['per_diem_total'] + management_crew['hourly_costs_total']) + self.output_dict['management_crew'] = management_crew + self.output_dict['managament_crew_cost_before_wind_delay']= management_crew['total_crew_cost_before_wind_delay'].sum() + + construction_time_output_data['operation_data_id_days_crews_workers'] = operation_data_id_days_crews_workers + construction_time_output_data['operation_data_entire_farm'] = operation_data + + return construction_time_output_data['operation_data_entire_farm'] + + def calculate_costs(self, calculate_costs_input_dict, calculate_costs_output_dict): + + #read in rsmeans data: + # rsmeans = calculate_costs_input_dict['rsmeans'] + operation_data = calculate_costs_output_dict['operation_data_entire_farm'] + + per_diem = operation_data['Number of workers'] * operation_data['Number of crews'] * (operation_data['Time construct days'] + np.ceil(operation_data['Time construct days'] / 7)) * calculate_costs_input_dict['rsmeans_per_diem'] + per_diem = per_diem.dropna() + + calculate_costs_output_dict['time_construct_days'] = (calculate_costs_output_dict['trench_length_km'] / self._km_to_LF) / calculate_costs_output_dict['trenching_labor_daily_output'] + wind_delay_fraction = (calculate_costs_output_dict['wind_delay_time'] / calculate_costs_input_dict['operational_hrs_per_day']) / calculate_costs_output_dict['time_construct_days'] + # check if wind_delay_fraction is greater than 1, which would mean weather delays are longer than they can possibily be for the input data + if wind_delay_fraction > 1: + raise ValueError('{}: Error: Wind delay greater than 100%'.format(type(self).__name__)) + calculate_costs_output_dict['wind_multiplier'] = 1 / (1 - wind_delay_fraction) + + #Calculating trenching cost: + calculate_costs_output_dict['Days taken for trenching (equipment)'] = (calculate_costs_output_dict['trench_length_km'] / self._km_to_LF) / calculate_costs_output_dict['trenching_equipment_daily_output'] + calculate_costs_output_dict['Equipment cost of trenching per day {usd/day)'] = calculate_costs_output_dict['trenching_cable_equipment_usd_per_hr'] * calculate_costs_input_dict['operational_hrs_per_day'] + calculate_costs_output_dict['Equipment Cost USD without weather delays'] = (calculate_costs_output_dict['Days taken for trenching (equipment)'] * calculate_costs_output_dict['Equipment cost of trenching per day {usd/day)']) + calculate_costs_output_dict['Equipment Cost USD with weather delays'] = calculate_costs_output_dict['Equipment Cost USD without weather delays'] * calculate_costs_output_dict['wind_multiplier'] + + trenching_equipment_rental_cost_df = pd.DataFrame([['Equipment rental',calculate_costs_output_dict['Equipment Cost USD with weather delays'], 'Collection']], + columns = ['Type of cost', 'Cost USD', 'Phase of construction']) + + #Calculating labor cost: + calculate_costs_output_dict['Days taken for trenching (labor)'] = ((calculate_costs_output_dict['trench_length_km'] / self._km_to_LF) / calculate_costs_output_dict['trenching_labor_daily_output']) + calculate_costs_output_dict['Labor cost of trenching per day (usd/day)'] = (calculate_costs_output_dict['trenching_labor_usd_per_hr'] * calculate_costs_input_dict['operational_hrs_per_day'] * calculate_costs_input_dict['overtime_multiplier']) + calculate_costs_output_dict['Total per diem costs (USD)'] = per_diem.sum() + calculate_costs_output_dict['Labor Cost USD without weather delays'] =((calculate_costs_output_dict['Days taken for trenching (labor)'] * calculate_costs_output_dict['Labor cost of trenching per day (usd/day)']) + (calculate_costs_output_dict['Total per diem costs (USD)'] + calculate_costs_output_dict['managament_crew_cost_before_wind_delay'])) + calculate_costs_output_dict['Labor Cost USD with weather delays'] = calculate_costs_output_dict['Labor Cost USD without weather delays'] * calculate_costs_output_dict['wind_multiplier'] + + trenching_labor_cost_df = pd.DataFrame([['Labor',calculate_costs_output_dict['Labor Cost USD with weather delays'], 'Collection']], + columns = ['Type of cost', 'Cost USD', 'Phase of construction']) + + #Calculate cable cost: + cable_cost_usd_per_LF_df = pd.DataFrame([['Materials',self._total_cable_cost, 'Collection']], + columns = ['Type of cost', 'Cost USD', 'Phase of construction']) + + # Combine all calculated cost items into the 'collection_cost' dataframe: + collection_cost = pd.DataFrame([],columns = ['Type of cost', 'Cost USD', 'Phase of construction']) # todo: I believe Phase of construction here is the same as Operation ID in other modules? we should change to be consistent + collection_cost = collection_cost.append(trenching_equipment_rental_cost_df) + collection_cost = collection_cost.append(trenching_labor_cost_df) + collection_cost = collection_cost.append(cable_cost_usd_per_LF_df) + + # Calculate Mobilization Cost and add to collection_cost dataframe: + mobilization_cost = pd.DataFrame([['Mobilization', collection_cost['Cost USD'].sum() * 0.05 , 'Collection']], + columns=['Type of cost', 'Cost USD', 'Phase of construction']) + collection_cost = collection_cost.append(mobilization_cost) + + calculate_costs_output_dict['total_collection_cost'] = collection_cost + + return collection_cost + + 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. + + Returns + ------- + list(dict) + A list of dicts, with each dict representing a row of the data. + """ + result = [] + module = 'Collection Cost' + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Total Number of Turbines', + 'value': float(self.output_dict['total_turb']) + }) + + result.append({ + 'unit': 'km', + 'type': 'variable', + 'variable_df_key_col_name': 'Total trench length', + 'value': float(self.output_dict['trench_length_km']) + }) + + result.append({ + 'unit': 'km', + 'type': 'variable', + 'variable_df_key_col_name': 'Total cable length', + 'value': float(self.output_dict['total_cable_len_km']) + }) + + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Turbines Per String in Full String', + 'value': float(self.output_dict['total_turb_per_string']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Full Strings', + 'value': float(self.output_dict['num_full_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Turbines in Partial String', + 'value': float(self.output_dict['num_leftover_turb']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Number of Partial Strings', + 'value': float(self.output_dict['num_partial_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Total number of strings full + partial', + 'value': float(self.output_dict['num_full_strings'] + self.output_dict['num_partial_strings']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Trench Length to Substation (km)', + 'value': float(self.output_dict['trench_len_to_substation_km']) + }) + result.append({ + 'unit': '', + 'type': 'variable', + 'variable_df_key_col_name': 'Cable Length to Substation (km)', + 'value': float(self.output_dict['cable_len_to_substation_km']) + }) + + cables = '' + n = 1 # to keep tab of number of cables input by user. + for cable, specs in self.output_dict['cables'].items(): + if n == len(self.output_dict['cables']): + cables += str(cable) + else: + cables += str(cable) + ' , ' + + for variable, value in specs.__dict__.items(): + if variable == 'array_cable_len': + result.append({ + 'unit': 'km', + 'type': 'variable', + 'variable_df_key_col_name': 'Array cable length for cable ' + cable, + 'value': float(value) + }) + elif variable == 'total_length': + result.append({ + 'unit': 'km', + 'type': 'variable', + 'variable_df_key_col_name': 'Total cable length for cable ' + cable, + 'value': float(value) + }) + + elif variable == 'total_cost': + result.append({ + 'unit': 'usd', + 'type': 'variable', + 'variable_df_key_col_name': 'Total cable cost for cable ' + cable, + 'value': float(value) + }) + n += 1 + + result.append({ + 'unit': '', + 'type': 'list', + 'variable_df_key_col_name': 'Number of turbines per cable type in full strings [' + cables + ']', + + 'value': str(self.output_dict['num_turb_per_cable']) + }) + + # self.output_dict['turb_per_partial_string'] is only available if + # self.output_dict['num_leftover_turb'] > 0 which is not always the + # case. Commenting this output out + + # result.append({ + # 'unit': '', + # 'type': 'list', + # 'variable_df_key_col_name': 'Number of turbines per cable type in partial string [' + cables + ']', + # + # 'value': str(self.output_dict['turb_per_partial_string']) + # }) + + result.append({ + 'unit': '', + 'type': 'list', + 'variable_df_key_col_name': 'Percent length of cable in partial string [' + cables + ']', + + 'value': str(self.output_dict['perc_partial_string']) + }) + + for row in self.output_dict['management_crew'].itertuples(): + dashed_row = ' <--> '.join(str(x) for x in list(row)) + result.append({ + 'unit': '', + 'type': 'dataframe', + 'variable_df_key_col_name': 'Labor type ID <--> Hourly rate USD per hour <--> Per diem USD per day <--> Operation <--> Crew type <--> Crew name <--> Number of workers <--> Per Diem Total <--> Hourly costs total <--> Crew total cost ', + 'value': dashed_row + }) + + for row in self.output_dict['total_collection_cost'].itertuples(): + dashed_row = '{} <--> {} <--> {}'.format(row[1], row[3], math.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['collection_cost_csv'] = result + return result + + def run_module(self): + """ + Runs the CollectionCost module and populates the IO dictionaries with calculated values. + + """ + + try: + self.create_ArraySystem() + self.calculate_trench_properties(self.input_dict, self.output_dict) + operation_data = self.estimate_construction_time(self.input_dict, self.output_dict) + + # pull only global inputs for weather delay from input_dict + weather_data_keys = ('wind_shear_exponent', + 'weather_window') + + # specify collection-specific weather delay inputs + self.weather_input_dict = dict( + [(i, self.input_dict[i]) for i in self.input_dict if i in set(weather_data_keys)]) + self.weather_input_dict[ + 'start_delay_hours'] = 0 # assume zero start for when collection construction begins (start at beginning of construction time) + self.weather_input_dict[ + 'critical_wind_speed_m_per_s'] = self.input_dict['critical_speed_non_erection_wind_delays_m_per_s'] + self.weather_input_dict[ + 'wind_height_of_interest_m'] = self.input_dict['critical_height_non_erection_wind_delays_m'] + + # compute and specify weather delay mission time for roads + duration_construction = operation_data['Time construct days'].max(skipna=True) + operational_hrs_per_day = self.input_dict['hour_day'][self.input_dict['time_construct']] + mission_time_hrs = duration_construction * operational_hrs_per_day + self.weather_input_dict['mission_time_hours'] = int(mission_time_hrs) + + self.calculate_weather_delay(self.weather_input_dict, self.output_dict) + self.calculate_costs(self.input_dict, self.output_dict) + self.outputs_for_detailed_tab(self.input_dict, self.output_dict) + self.output_dict['collection_cost_module_type_operation'] = self.outputs_for_costs_by_module_type_operation( + input_df=self.output_dict['total_collection_cost'], + project_id=self.project_name, + total_or_turbine=True + ) + return 0, 0 # module ran successfully + except Exception as error: + traceback.print_exc() + print(f"Fail {self.project_name} CollectionCost") + return 1, error # module did not run successfully diff --git a/landbosse/model/CostModule.py b/landbosse/model/CostModule.py index 5690e9b7..5ff204b6 100644 --- a/landbosse/model/CostModule.py +++ b/landbosse/model/CostModule.py @@ -1,4 +1,5 @@ import math +import numpy as np class CostModule: """ @@ -7,6 +8,22 @@ class CostModule: mobilization cost calculations. """ + def layout_length(self): + """ In manual mode, calculates the total length between nodes in the farm, which is the road length, and cable + length from substation to all turbines [km].""" + self.output_dict['layout_length_km'] = 0 + self.collection_layout = self.input_dict['collection_layout'].values + self.L = self.collection_layout[:, :2] # location of nodes [m] + self.A = self.collection_layout[:, 2:] # adjacency matrix for collection system. Zeroth element is substation + dim = self.A.shape + self.n_segments = dim[1] - 1 # #turbines + remains = np.ones(self.n_segments + 1) # turbines still to have cables defined around + for i in range(0, self.n_segments): + for j in np.where(self.A[i, :] * remains == 1)[0]: + self.output_dict['layout_length_km'] += ((self.L[i, 0] - self.L[j, 0]) ** 2 + ( + self.L[i, 1] - self.L[j, 1]) ** 2) ** (1 / 2) + remains[i] = False # prevent duplicate cables + def mobilization_cost(self, turbine_rating): """ Calculates a mobilization cost term as a function of diff --git a/landbosse/model/ErectionCost.py b/landbosse/model/ErectionCost.py index 250a52fa..8d60fcd2 100644 --- a/landbosse/model/ErectionCost.py +++ b/landbosse/model/ErectionCost.py @@ -316,7 +316,6 @@ def calculate_erection_operation_time(self): hub_height_m = self.input_dict['hub_height_meters'] rotor_diameter_m = self.input_dict['rotor_diameter_m'] num_turbines = float(self.input_dict['num_turbines']) - turbine_spacing_rotor_diameters = self.input_dict['turbine_spacing_rotor_diameters'] # for components in component list determine if base or topping project_data['components']['Operation'] = project_data['components']['Lift height m'] > ( @@ -359,9 +358,13 @@ def calculate_erection_operation_time(self): drop=True) # calculate travel time per cycle - turbine_spacing = float( - turbine_spacing_rotor_diameters * rotor_diameter_m * km_per_m) - possible_cranes['Travel time hr'] = turbine_spacing / possible_cranes['Speed of travel km per hr'] * num_turbines + if self.input_dict['collection_mode'] == 'auto': + turbine_spacing_rotor_diameters = self.input_dict['turbine_spacing_rotor_diameters'] + dist = float(turbine_spacing_rotor_diameters * rotor_diameter_m * km_per_m) * num_turbines + else: + self.layout_length() + dist = self.output_dict['layout_length_km'] + possible_cranes['Travel time hr'] = dist / possible_cranes['Speed of travel km per hr'] # calculate erection time possible_cranes['Operation time hr'] = ((possible_cranes['Lift height m'] / possible_cranes[ @@ -458,8 +461,7 @@ def calculate_offload_operation_time(self): operational_construction_time = self.input_dict['operational_construction_time'] rate_of_deliveries = self.input_dict['rate_of_deliveries'] rotor_diameter_m = self.input_dict['rotor_diameter_m'] - num_turbines = float(self.input_dict['num_turbines']) - turbine_spacing_rotor_diameters = self.input_dict['turbine_spacing_rotor_diameters'] + num_turbines = float(self.input_dict['num_turbines'])\ offload_cranes = project_data['crane_specs'].where( project_data['crane_specs']['Equipment name'] == 'Offload crane') @@ -487,19 +489,22 @@ def calculate_offload_operation_time(self): drop=True) # calculate travel time per cycle - turbine_spacing = float( - turbine_spacing_rotor_diameters * rotor_diameter_m * km_per_m) - turbine_num = float(self.input_dict['num_turbines']) - possible_cranes['Travel time hr'] = turbine_spacing / possible_cranes['Speed of travel km per hr'] * num_turbines + if self.input_dict['collection_mode'] == 'auto': + turbine_spacing_rotor_diameters = self.input_dict['turbine_spacing_rotor_diameters'] + dist = float(turbine_spacing_rotor_diameters * rotor_diameter_m * km_per_m) * num_turbines + else: + self.layout_length() + dist = self.output_dict['layout_length_km'] + possible_cranes['Travel time hr'] = dist / possible_cranes['Speed of travel km per hr'] # calculate erection time possible_cranes['Operation time hr'] = ((possible_cranes['Lift height m'] / possible_cranes[ 'Hoist speed m per min'] * hr_per_min) + (possible_cranes['Offload cycle time hrs']) - ) * turbine_num + ) * num_turbines # store setup time - possible_cranes['Setup time hr'] = possible_cranes['Setup time hr'] * turbine_num + possible_cranes['Setup time hr'] = possible_cranes['Setup time hr'] * num_turbines erection_time = \ possible_cranes.groupby(['Crane name', 'Equipment name', 'Crane capacity tonne', 'Crew type ID', @@ -520,9 +525,9 @@ def calculate_offload_operation_time(self): # if more than one crew needed to complete within construction duration # then assume that all construction happens within that window and use # that timeframe for weather delays; if not, use the number of days calculated - operation_time['time_construct_bool'] = (turbine_num / operation_time['Operational construct days'] * 6 + operation_time['time_construct_bool'] = (num_turbines / operation_time['Operational construct days'] * 6 > float(rate_of_deliveries)) - boolean_dictionary = {True: (float(turbine_num) / (float(rate_of_deliveries) / 6)), False: np.NAN} + boolean_dictionary = {True: (float(num_turbines) / (float(rate_of_deliveries) / 6)), False: np.NAN} operation_time['time_construct_bool'] = operation_time['time_construct_bool'].map(boolean_dictionary) operation_time['Time construct days'] = operation_time[ ['time_construct_bool', 'Operational construct days']].max( diff --git a/landbosse/model/Manager.py b/landbosse/model/Manager.py index 164ee4a5..4c96a483 100644 --- a/landbosse/model/Manager.py +++ b/landbosse/model/Manager.py @@ -48,6 +48,9 @@ def execute_landbosse(self, project_name): self.input_dict['weather_window'] = filtered_weather_window self.input_dict['weather_data_user_input'] = weather_data_user_input + collection_cost = ArraySystem(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) + collection_cost.run_module() + foundation_cost = FoundationCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) foundation_cost.run_module() @@ -60,9 +63,6 @@ def execute_landbosse(self, project_name): transdist_cost = GridConnectionCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) transdist_cost.run_module() - collection_cost = ArraySystem(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) - collection_cost.run_module() - development_cost = DevelopmentCost(input_dict=self.input_dict, output_dict=self.output_dict, project_name=project_name) development_cost.run_module() diff --git a/landbosse/model/SitePreparationCost.py b/landbosse/model/SitePreparationCost.py index 3cd78987..5c04790c 100644 --- a/landbosse/model/SitePreparationCost.py +++ b/landbosse/model/SitePreparationCost.py @@ -157,6 +157,7 @@ class documentation self._meters_per_inch = 0.025 self._cubic_yards_per_cubic_meter = 1.30795 self._square_feet_per_square_meter = 10.7639 + self._mi_to_m = 1609.34 # cubic meters for crane pad and maintenance ring for each turbine # (from old BOS model - AI - Access Roads & Site Imp. tab cell J33) @@ -210,9 +211,14 @@ def calculate_road_properties(self, road_properties_input, road_properties_outpu """ - - road_properties_output['road_length_m'] = ((road_properties_input['num_turbines'] - 1) * road_properties_input['turbine_spacing_rotor_diameters'] * road_properties_input['rotor_diameter_m']) + road_properties_input['road_length_adder_m'] - + if self.input_dict['collection_mode'] == 'manual': + self.layout_length() + road_properties_output['road_length_m'] = self.output_dict['layout_length_km'] * 1000 + road_properties_input['road_length_adder_m'] + else: + road_properties_output['road_length_m'] = ((road_properties_input['num_turbines'] - 1) * + road_properties_input['turbine_spacing_rotor_diameters'] * + road_properties_input['rotor_diameter_m']) + \ + road_properties_input['road_length_adder_m'] # units of cubic meters road_properties_output['road_volume'] = road_properties_output['road_length_m'] * \ (road_properties_input['road_width_ft'] * self._meters_per_foot) * \ diff --git a/project_input_template/inputs.zip b/project_input_template/inputs.zip new file mode 100644 index 00000000..9b4ab032 Binary files /dev/null and b/project_input_template/inputs.zip differ diff --git a/project_input_template/project_data/project_1.xlsx b/project_input_template/project_data/foundation_validation_ge15.xlsx similarity index 91% rename from project_input_template/project_data/project_1.xlsx rename to project_input_template/project_data/foundation_validation_ge15.xlsx index 076940b6..445224bf 100644 Binary files a/project_input_template/project_data/project_1.xlsx and b/project_input_template/project_data/foundation_validation_ge15.xlsx differ diff --git a/project_input_template/project_data/ge15_project_data.xlsx b/project_input_template/project_data/ge15_project_data.xlsx new file mode 100644 index 00000000..ac8c5a11 Binary files /dev/null and b/project_input_template/project_data/ge15_project_data.xlsx differ diff --git a/project_input_template/project_data/northwind_100_dist_project_data.xlsx b/project_input_template/project_data/northwind_100_dist_project_data.xlsx new file mode 100644 index 00000000..2628c4e8 Binary files /dev/null and b/project_input_template/project_data/northwind_100_dist_project_data.xlsx differ diff --git a/project_input_template/project_data/project_data_defaults.xlsx b/project_input_template/project_data/project_data_defaults.xlsx new file mode 100644 index 00000000..591def4d Binary files /dev/null and b/project_input_template/project_data/project_data_defaults.xlsx differ diff --git a/project_input_template/project_data/validation_northwind100_dw.xlsx b/project_input_template/project_data/validation_northwind100_dw.xlsx new file mode 100644 index 00000000..44502bea Binary files /dev/null and b/project_input_template/project_data/validation_northwind100_dw.xlsx differ diff --git a/project_input_template/project_list.xlsx b/project_input_template/project_list.xlsx index 545d52ee..06ada2ad 100644 Binary files a/project_input_template/project_list.xlsx and b/project_input_template/project_list.xlsx differ