diff --git a/ORBIT/core/defaults/common_costs.yaml b/ORBIT/core/defaults/common_costs.yaml index 2237a44d..1e8e9867 100644 --- a/ORBIT/core/defaults/common_costs.yaml +++ b/ORBIT/core/defaults/common_costs.yaml @@ -1,6 +1,72 @@ # Material costs -monopile_steel_cost: 3000 # USD/t -tp_steel_cost: 3000 # USD/t +monopile_design: + monopile_steel_cost: 3000 # USD/t + tp_steel_cost: 3000 # USD/t # Port properties port_cost_per_month: 2e6 # USD/month + +# Export system component cost rates +export_system_design: + cable_crossings: + crossing_unit_cost: 500000 + +# Spar component cost rates +spar_design: + stiffened_column_CR: 3120 # USD/t + tapered_column_CR: 4220 # USD/t + ballast_material_CR: 100 # USD/t + secondary_steel_CR: 7250 # USD/t + +# Offshore substation component cost rates +substation_design: + mpt_cost_rate: 12500 # USD/MW + topside_fab_cost_rate: 14500 # USD/t + topside_design_cost: # USD + #oldHVAC: 4.5e6 + HVAC: 107.3e6 + HVDC-monopole: 294e6 + HVDC-bipole: 476e6 + shunt_cost_rate: 35000 # USD/MW + mpt_unit_cost: 2.87e6 # USD/mpt + shunt_unit_cost: 10000 # USD/cable + switchgear_cost: 4e6 # USD/cable + dc_breaker_cost: 10.5e6 # USD/cable + backup_gen_cost: 1e6 # USD + workspace_cost: 2e6 # USD + other_ancillary_cost: 3e6 # USD + topside_assembly_factor: 0.075 # % + converter_cost: # USD + HVAC: 0 + HVDC-monopole: 127e6 + HVDC-bipole: 296e6 + oss_substructure_cost_rate: 3000 # USD/t + oss_pile_cost_rate: 0 # USD/t + +# Onshore substation component cost rates +onshore_substation_design: + onshore_converter_cost: # USD + HVAC: 0 + HVDC-monopole: 157e6 + HVDC-bipole: 350e6 + shunt_unit_cost: 13000 # USD/cable + switchgear_cost: 9.33e6 # USD/cable + compensation_rate: # USD/cable + HVAC: 31.3e6 + HVDC-monopole: 0 + HVDC-bipole: 0 + onshore_construction_rate: # USD + HVAC: 5e6 + HVDC-monopole: 87.3e6 + HVDC-bipole: 100e6 + +# Semisubmersible component cost rates +semisubmersible_design: + stiffened_column_CR: 3120 # USD/t + truss_CR: 6250 # USD/t + heave_plate_CR: 6250 # USD/t + secondary_steel_CR: 7250 # USD/t + +# Mooring system component cost rates +mooring_system_design: # USD/m + mooring_line_cost_rate: [399.0, 721.0, 1088.0] diff --git a/ORBIT/phases/design/design_phase.py b/ORBIT/phases/design/design_phase.py index a27ba0b1..acf7b829 100644 --- a/ORBIT/phases/design/design_phase.py +++ b/ORBIT/phases/design/design_phase.py @@ -9,6 +9,7 @@ from abc import abstractmethod from ORBIT.phases import BasePhase +from ORBIT.core.defaults import common_costs class DesignPhase(BasePhase): @@ -31,3 +32,31 @@ def design_result(self): """ return {} + + def get_default_cost(self, design_name, key, subkey=None): + """Return the cost value for a key in a design + dictionary read from common_cost.yaml. + """ + + if (design_dict := common_costs.get(design_name, None)) is None: + raise KeyError(f"No {design_name} in common_cost.yaml.") + + if (cost_value := design_dict.get(key, None)) is None: + raise KeyError(f"{key} not found in [{design_name}] common_costs.") + + if isinstance(cost_value, dict): + if subkey is None: + raise ValueError( + f"{key} is a dictionary and requires a 'subkey' input." + ) + + if (sub_cost_value := cost_value.get(subkey, None)) is None: + raise KeyError( + f"{subkey} not found in [{design_name}][{cost_value}]" + " common_costs." + ) + + return sub_cost_value + + else: + return cost_value diff --git a/ORBIT/phases/design/electrical_export.py b/ORBIT/phases/design/electrical_export.py index 704d5177..2ca34bfe 100644 --- a/ORBIT/phases/design/electrical_export.py +++ b/ORBIT/phases/design/electrical_export.py @@ -11,6 +11,11 @@ from ORBIT.phases.design._cables import CableSystem +""" +[1] Maness et al. 2017, NREL Offshore Balance-of-System Model. +https://www.nrel.gov/docs/fy17osti/66874.pdf +""" + class ElectricalDesign(CableSystem): """ @@ -93,6 +98,7 @@ class ElectricalDesign(CableSystem): "cable_type": "str", }, }, + "offshore_substation": "dict, (optional)", } def __init__(self, config, **kwargs): @@ -106,11 +112,14 @@ def __init__(self, config, **kwargs): for name in self.expected_config["site"]: setattr(self, "".join(("_", name)), config["site"][name]) + self._depth = config["site"]["depth"] self._distance_to_landfall = config["site"]["distance_to_landfall"] self._plant_capacity = self.config["plant"]["capacity"] self._get_touchdown_distance() + self._design = self.config["export_system_design"] + _landfall = self.config.get("landfall", {}) if _landfall: warn( @@ -121,25 +130,18 @@ def __init__(self, config, **kwargs): ) else: - _landfall = self.config["export_system_design"].get("landfall", {}) + _landfall = self._design.get("landfall", {}) self._distance_to_interconnection = _landfall.get( "interconnection_distance", 3 ) - self.export_system_design = self.config["export_system_design"] - self.offshore_substation_design = self.config.get( - "substation_design", {} - ) + self._oss_design = self.config.get("substation_design", {}) - self.substructure_type = self.offshore_substation_design.get( + self.substructure_type = self._oss_design.get( "oss_substructure_type", "Monopile" ) - self.onshore_substation_design = self.config.get( - "onshore_substation_design", {} - ) - self._outputs = {} def run(self): @@ -187,6 +189,16 @@ def run(self): self.calc_dc_breaker_cost() self.calc_onshore_cost() + self._outputs["offshore_substation"] = { + "substation_mpt_cost": self.mpt_cost, + "substation_shunt_cost": self.shunt_reactor_cost, + "substation_switchgear_cost": self.switchgear_cost, + "substation_converter_cost": self.converter_cost, + "substation_breaker_cost": self.dc_breaker_cost, + "substation_ancillary_cost": self.ancillary_system_costs, + "substation_land_assembly_cost": self.land_assembly_cost, + } + self._outputs["offshore_substation_substructure"] = { "type": self.substructure_type, "deck_space": self.substructure_deck_space, @@ -222,6 +234,13 @@ def detailed_output(self): "substation_substructure_mass": self.substructure_mass, "substation_substructure_cost": self.substructure_cost, "total_substation_cost": self.total_substation_cost, + "substation_mpt_cost": self.mpt_cost, + "substation_shunt_cost": self.shunt_reactor_cost, + "substation_switchgear_cost": self.switchgear_cost, + "substation_converter_cost": self.converter_cost, + "substation_breaker_cost": self.dc_breaker_cost, + "substation_ancillary_cost": self.ancillary_system_costs, + "substation_land_assembly_cost": self.land_assembly_cost, "onshore_shunt_cost": self.onshore_shunt_reactor_cost, "onshore_converter_cost": self.onshore_converter_cost, "onshore_switchgear_cost": self.onshore_switchgear_cost, @@ -250,13 +269,8 @@ def compute_number_cables(self): Calculate the total number of required and redundant cables to transmit power to the onshore interconnection. - Parameters - ---------- - num_redundant : int """ - _num_redundant = self._design.get("num_redundant", 0) - num_required = np.ceil(self._plant_capacity / self.cable.cable_power) num_redundant = self._design.get("num_redundant", 0) @@ -321,12 +335,19 @@ def sections_cables(self): def calc_crossing_cost(self): """Compute cable crossing costs.""" - self._crossing_design = self.config["export_system_design"].get( - "cable_crossings", {} + _crossing_design = self._design.get("cable_crossings", {}) + + _key = "crossing_unit_cost" + crossing_cost = _crossing_design.get( + _key, + self.get_default_cost( + "export_system_design", "cable_crossings", subkey=_key + ), + ) + + self.crossing_cost = crossing_cost * _crossing_design.get( + "crossing_number", 0 ) - self.crossing_cost = self._crossing_design.get( - "crossing_unit_cost", 500000 - ) * self._crossing_design.get("crossing_number", 0) """SUBSTATION""" @@ -341,23 +362,19 @@ def total_substation_cost(self): def calc_num_substations(self): """Computes number of substations based on HVDC or HVAC export cables. - - Parameters - ---------- - substation_capacity : int | float """ # HVAC substation capacity - _substation_capacity = self.offshore_substation_design.get( + _substation_capacity = self._oss_design.get( "substation_capacity", 1200 ) # MW if "HVDC" in self.cable.cable_type: - self.num_substations = self.offshore_substation_design.get( + self.num_substations = self._oss_design.get( "num_substations", int(self.num_cables / 2) ) else: - self.num_substations = self.offshore_substation_design.get( + self.num_substations = self._oss_design.get( "num_substations", int(np.ceil(self._plant_capacity / _substation_capacity)), ) @@ -377,72 +394,69 @@ def substation_cost(self): ) / self.num_substations def calc_mpt_cost(self): - """Computes HVAC main power transformer (MPT). MPT cost is 0 for HVDC. - - Parameters - ---------- - mpt_unit_cost : int | float + """Computes HVAC main power transformer (MPT). MPT cost is 0 for + HVDC. """ - _mpt_cost = self._design.get("mpt_unit_cost", 2.87e6) + _key = "mpt_unit_cost" + _mpt_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) self.num_mpt = self.num_cables - if "HVDC" in self.cable.cable_type: - self.mpt_cost = 0 - - else: - self.mpt_cost = self.num_mpt * _mpt_cost + self.mpt_cost = ( + 0 if "HVDC" in self.cable.cable_type else self.num_mpt * _mpt_cost + ) self.mpt_rating = ( round((self._plant_capacity * 1.15 / self.num_mpt) / 10.0) * 10.0 ) def calc_shunt_reactor_cost(self): - """Computes HVAC shunt reactor cost. Shunt reactor cost is 0 for HVDC. - - Parameters - ---------- - shunt_unit_cost : int | float + """Computes HVAC shunt reactor cost. Shunt reactor cost is 0 for + HVDC. """ touchdown = self.config["site"]["distance_to_landfall"] - shunt_unit_cost = self._design.get("shunt_unit_cost", 1e4) + + _key = "shunt_unit_cost" + + shunt_unit_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) if "HVDC" in self.cable.cable_type: self.compensation = 0 else: for cable in self.cables.values(): self.compensation = touchdown * cable.compensation_factor # MW + self.shunt_reactor_cost = ( self.compensation * shunt_unit_cost * self.num_cables ) def calc_switchgear_costs(self): - """Computes HVAC switchgear cost. Switchgear cost is 0 for HVDC. + """Computes HVAC switchgear cost. Switchgear cost is 0 for HVDC.""" - Parameters - ---------- - switchgear_cost : int | float - """ - - switchgear_cost = self._design.get("switchgear_cost", 4e6) + _key = "switchgear_cost" + switchgear_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) - num_switchgear = ( + self.num_switchgear = ( 0 if "HVDC" in self.cable.cable_type else self.num_cables ) - self.switchgear_cost = num_switchgear * switchgear_cost + self.switchgear_cost = self.num_switchgear * switchgear_cost def calc_dc_breaker_cost(self): - """Computes HVDC circuit breaker cost. Breaker cost is 0 for HVAC. + """Computes HVDC circuit breaker cost. Breaker cost is 0 for HVAC.""" - Parameters - ---------- - dc_breaker_cost : int | float - """ - - dc_breaker_cost = self._design.get("dc_breaker_cost", 10.5e6) + _key = "dc_breaker_cost" + dc_breaker_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) num_dc_breakers = ( self.num_cables if "HVDC" in self.cable.cable_type else 0 @@ -451,88 +465,83 @@ def calc_dc_breaker_cost(self): self.dc_breaker_cost = num_dc_breakers * dc_breaker_cost def calc_ancillary_system_cost(self): - """ - Calculates cost of ancillary systems. + """Calculates cost of ancillary systems.""" - Parameters - ---------- - backup_gen_cost : int | float - workspace_cost : int | float - other_ancillary_cost : int | float - """ + _key = "backup_gen_cost" + backup_gen_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) - backup_gen_cost = self._design.get("backup_gen_cost", 1e6) - workspace_cost = self._design.get("workspace_cost", 2e6) - other_ancillary_cost = self._design.get("other_ancillary_cost", 3e6) + _key = "workspace_cost" + workspace_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) + + _key = "other_ancillary_cost" + other_ancillary_cost = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) + ) self.ancillary_system_costs = ( backup_gen_cost + workspace_cost + other_ancillary_cost ) * self.num_substations - def calc_converter_cost(self): - """Computes converter cost.""" - - if self.cable.cable_type == "HVDC-monopole": - self.converter_cost = self.num_substations * self._design.get( - "converter_cost", 127e6 - ) - - elif self.cable.cable_type == "HVDC-bipole": - self.converter_cost = self.num_substations * self._design.get( - "converter_cost", 296e6 - ) - else: - self.converter_cost = 0 - def calc_assembly_cost(self): - """ - Calculates the cost of assembly on land. - - Parameters - ---------- - topside_assembly_factor : int | float - """ + """Calculates the cost of assembly on land.""" - topside_assembly_factor = self.offshore_substation_design.get( - "topside_assembly_factor", 0.075 + _key = "topside_assembly_factor" + topside_assembly_factor = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) ) + if topside_assembly_factor > 1.0: + topside_assembly_factor /= 100 + self.land_assembly_cost = ( self.switchgear_cost + self.shunt_reactor_cost + self.ancillary_system_costs ) * topside_assembly_factor + def calc_converter_cost(self): + """Computes converter cost.""" + + _key = "converter_cost" + converter_cost = self._oss_design.get( + _key, + self.get_default_cost( + "substation_design", _key, subkey=self.cable.cable_type + ), + ) + + self.converter_cost = converter_cost + def calc_substructure_mass_and_cost(self): """ - Calculates the mass and associated cost of the substation substructure. - - Parameters - ---------- - oss_substructure_cost_rate : int | float - oss_pile_cost_rate : int | float + Calculates the mass and associated cost of the substation substructure + based on equations 81-84 [1]. """ - oss_pile_cost_rate = self.offshore_substation_design.get( - "oss_pile_cost_rate", 0 + _key = "oss_substructure_cost_rate" + oss_substructure_cost_rate = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) ) - oss_substructure_cost_rate = self.offshore_substation_design.get( - "oss_substructure_cost_rate", 3000 + + _key = "oss_pile_cost_rate" + oss_pile_cost_rate = self._oss_design.get( + _key, self.get_default_cost("substation_design", _key) ) - # Substructure mass components calculated by curve fits in - # equations 81-84 from Maness et al. 2017 - # https://www.nrel.gov/docs/fy17osti/66874.pdf - # + # Substructure mass components # TODO: Determine a better method to calculate substructure mass # for different substructure types substructure_mass = 0.4 * self.topside_mass - if self.substructure_type == "Floating": - substructure_pile_mass = 0 # No piles used for floating platform - - else: - substructure_pile_mass = 8 * substructure_mass**0.5574 + substructure_pile_mass = ( + 0 + if "Floating" in self.substructure_type + else 8 * substructure_mass**0.5574 + ) self.substructure_cost = ( substructure_mass * oss_substructure_cost_rate @@ -569,81 +578,76 @@ def calc_topside_deck_space(self): self.topside_deck_space = 1 def calc_topside_mass_and_cost(self): - """ - Calculates the mass and cost of the substation topsides. - - Parameters - ---------- - topside_design_cost: int | float - """ - - _design = self.config.get("substation_design", {}) + """Calculates the mass and cost of the substation topsides.""" self.topside_mass = ( 3.85 * (self.mpt_rating * self.num_mpt) / self.num_substations + 285 ) - if self.cable.cable_type == "HVDC-monopole": - self.topside_cost = _design.get("topside_design_cost", 294e6) - elif self.cable.cable_type == "HVDC-bipole": - self.topside_cost = _design.get("topside_design_cost", 476e6) - else: - self.topside_cost = _design.get("topside_design_cost", 107.3e6) - def calc_onshore_cost(self): - """Minimum Cost of Onshore Substation Connection. + _key = "topside_design_cost" + topside_design_cost = self._oss_design.get( + _key, + self.get_default_cost( + "substation_design", _key, subkey=self.cable.cable_type + ), + ) - Parameters - ---------- - shunt_unit_cost : int | float - onshore_converter_cost: int | float - switchgear_cost: int | float - """ + self.topside_cost = topside_design_cost + + def calc_onshore_cost(self): + """Minimum Cost of Onshore Substation Connection.""" _design = self.config.get("onshore_substation_design", {}) - _shunt_unit_cost = _design.get("shunt_unit_cost", 1.3e4) # per cable - _switchgear_cost = _design.get("switchgear_cost", 9.33e6) # per cable - _compensation_rate = _design.get( - "compensation_rate", 31.3e6 - ) # per cable + _key = "onshore_converter_cost" + _converter_cost = _design.get( + _key, + self.get_default_cost( + "onshore_substation_design", _key, subkey=self.cable.cable_type + ), + ) + + self.onshore_converter_cost = self.num_substations * _converter_cost + + _key = "switchgear_cost" + _switchgear_cost = _design.get( + _key, self.get_default_cost("onshore_substation_design", _key) + ) + + self.onshore_switchgear_cost = self.num_switchgear * _switchgear_cost + + _key = "onshore_construction_rate" + _construction_rate = _design.get( + _key, + self.get_default_cost( + "onshore_substation_design", _key, subkey=self.cable.cable_type + ), + ) + + self.onshore_construction = self.num_substations * _construction_rate + + _key = "shunt_unit_cost" + _shunt_unit_cost = _design.get( + _key, self.get_default_cost("onshore_substation_design", _key) + ) self.onshore_shunt_reactor_cost = ( self.compensation * self.num_cables * _shunt_unit_cost ) - if self.cable.cable_type == "HVDC-monopole": - self.onshore_converter_cost = ( - self.num_substations - * self._design.get("onshore_converter_cost", 157e6) - ) - self.onshore_switchgear_cost = 0 - self.onshore_construction = self.num_substations * _design.get( - "onshore_construction_rate", 87.3e6 - ) - self.onshore_compensation_cost = 0 - - elif self.cable.cable_type == "HVDC-bipole": - self.onshore_converter_cost = ( - self.num_substations - * self._design.get("onshore_converter_cost", 350e6) - ) - self.onshore_switchgear_cost = 0 - self.onshore_construction = self.num_substations * _design.get( - "onshore_construction_rate", 100e6 - ) - self.onshore_compensation_cost = 0 + _key = "compensation_rate" + _compensation_rate = _design.get( + _key, + self.get_default_cost( + "onshore_substation_design", _key, subkey=self.cable.cable_type + ), + ) - else: - self.onshore_converter_cost = 0 - self.onshore_switchgear_cost = self.num_cables * _switchgear_cost - self.onshore_construction = self.num_substations * _design.get( - "onshore_construction_rate", 5e6 - ) - self.onshore_compensation_cost = ( - self.num_cables * _compensation_rate - + self.onshore_shunt_reactor_cost - ) + self.onshore_compensation_cost = ( + self.num_cables * _compensation_rate + + self.onshore_shunt_reactor_cost + ) self.onshore_cost = ( self.onshore_converter_cost diff --git a/ORBIT/phases/design/monopile_design.py b/ORBIT/phases/design/monopile_design.py index 8fb33d62..459333b5 100644 --- a/ORBIT/phases/design/monopile_design.py +++ b/ORBIT/phases/design/monopile_design.py @@ -10,7 +10,6 @@ from scipy.optimize import fsolve -from ORBIT.core.defaults import common_costs from ORBIT.phases.design import DesignPhase @@ -75,6 +74,8 @@ def __init__(self, config, **kwargs): config = self.initialize_library(config, **kwargs) self.config = self.validate_config(config) + self._design = self.config.get("monopile_design", {}) + self._outputs = {} def run(self): @@ -306,14 +307,10 @@ def total_tp_mass(self): def monopile_steel_cost(self): """Returns the cost of monopile steel (USD/t) fully fabricated.""" - _design = self.config.get("monopile_design", {}) _key = "monopile_steel_cost" - - try: - cost = _design.get(_key, common_costs[_key]) - - except KeyError as exc: - raise Exception("Cost of monopile steel not found.") from exc + cost = self._design.get( + _key, self.get_default_cost("monopile_design", _key) + ) return cost @@ -321,16 +318,10 @@ def monopile_steel_cost(self): def tp_steel_cost(self): """Returns the cost of fabricated transition piece steel (USD/t).""" - _design = self.config.get("monopile_design", {}) _key = "tp_steel_cost" - - try: - cost = _design.get(_key, common_costs[_key]) - - except KeyError as exc: - raise Exception( - "Cost of transition piece steel not found." - ) from exc # noqa: E501 + cost = self._design.get( + _key, self.get_default_cost("monopile_design", _key) + ) return cost diff --git a/ORBIT/phases/design/mooring_system_design.py b/ORBIT/phases/design/mooring_system_design.py index 7e703c99..ff41c1d1 100644 --- a/ORBIT/phases/design/mooring_system_design.py +++ b/ORBIT/phases/design/mooring_system_design.py @@ -14,6 +14,14 @@ from ORBIT.phases.design import DesignPhase +""" +[1] Maness et al. 2017, NREL Offshore Balance-of-System Model. +https://www.nrel.gov/docs/fy17osti/66874.pdf + +[2] Cooperman et al. (2022), Assessment of Offshore Wind Energy Leasing Areas +for Humboldt and Morry Bay. https://www.nrel.gov/docs/fy22osti/82341.pdf +""" + class MooringSystemDesign(DesignPhase): """Mooring System and Anchor Design.""" @@ -68,8 +76,7 @@ def __init__(self, config, **kwargs): self.anchor_type = self._design.get("anchor_type", "Suction Pile") self.mooring_type = self._design.get("mooring_type", "Catenary") - # Semi-Taut mooring system design parameters based on depth - # Cooperman et al. (2022), https://www.nrel.gov/docs/fy22osti/82341.pdf + # Semi-Taut mooring system design parameters based on depth [2]. self._semitaut_params = { "depths": [500.0, 750.0, 1000.0, 1250.0, 1500.0], "rope_lengths": [478.41, 830.34, 1229.98, 1183.93, 1079.62], @@ -108,26 +115,32 @@ def determine_mooring_line(self): tr = self.config["turbine"]["turbine_rating"] fit = -0.0004 * (tr**2) + 0.0132 * tr + 0.0536 + _key = "mooring_line_cost_rate" + + mooring_line_cost_rate = self._design.get( + _key, + self.get_default_cost( + "mooring_system_design", + _key, + ), + ) + if isinstance(mooring_line_cost_rate, (int, float)): + mooring_line_cost_rate = [mooring_line_cost_rate] * 3 + if fit <= 0.09: self.line_diam = 0.09 self.line_mass_per_m = 0.161 - self.line_cost_rate = self._design.get( - "mooring_line_cost_rate", 399.0 - ) + self.line_cost_rate = mooring_line_cost_rate[0] elif fit <= 0.12: self.line_diam = 0.12 self.line_mass_per_m = 0.288 - self.line_cost_rate = self._design.get( - "mooring_line_cost_rate", 721.0 - ) + self.line_cost_rate = mooring_line_cost_rate[1] else: self.line_diam = 0.15 self.line_mass_per_m = 0.450 - self.line_cost_rate = self._design.get( - "mooring_line_cost_rate", 1088.0 - ) + self.line_cost_rate = mooring_line_cost_rate[2] def calculate_breaking_load(self): """Returns the mooring line breaking load.""" @@ -214,8 +227,8 @@ def calculate_anchor_mass_cost(self): """ Returns the mass and cost of anchors. - TODO: Anchor masses are rough estimates based on initial literature - review. Should be revised when this module is overhauled in the future. + TODO: Anchor masses are rough estimates based on [1]. Should be + revised when this module is overhauled in the future. TODO: Mooring types for Catenary, TLP, SemiTaut will likely have different anchors. """ diff --git a/ORBIT/phases/design/oss_design.py b/ORBIT/phases/design/oss_design.py index 1c276550..9418e2fc 100644 --- a/ORBIT/phases/design/oss_design.py +++ b/ORBIT/phases/design/oss_design.py @@ -32,6 +32,12 @@ class OffshoreSubstationDesign(DesignPhase): "oss_pile_cost_rate": "USD/t (optional)", "num_substations": "int (optional)", }, + # "export_system": { + # "cable": { + # "number": "int", + # "cable_type": "str", + # }, + # }, } output_config = { @@ -51,6 +57,8 @@ def __init__(self, config, **kwargs): config = self.initialize_library(config, **kwargs) self.config = self.validate_config(config) + self._design = self.config.get("substation_design", {}) + self._outputs = {} def run(self): @@ -143,13 +151,11 @@ def calc_num_mpt_and_rating(self): turbine_rating : float """ - _design = self.config.get("substation_design", {}) - num_turbines = self.config["plant"]["num_turbines"] turbine_rating = self.config["turbine"]["turbine_rating"] capacity = num_turbines * turbine_rating - self.num_substations = _design.get( + self.num_substations = self._design.get( "num_substations", int(np.ceil(capacity / 1200)) ) self.num_mpt = np.ceil( @@ -175,8 +181,10 @@ def calc_mpt_cost(self): mpt_cost_rate : float """ - _design = self.config.get("substation_design", {}) - mpt_cost_rate = _design.get("mpt_cost_rate", 12500) + _key = "mpt_cost_rate" + mpt_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) self.mpt_cost = self.mpt_rating * self.num_mpt * mpt_cost_rate @@ -190,9 +198,16 @@ def calc_topside_mass_and_cost(self): topside_design_cost: int | float """ - _design = self.config.get("substation_design", {}) - topside_fab_cost_rate = _design.get("topside_fab_cost_rate", 14500) - topside_design_cost = _design.get("topside_design_cost", 4.5e6) + _key = "topside_fab_cost_rate" + topside_fab_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) + + _key = "topside_design_cost" + topside_design_cost = self._design.get( + _key, + self.get_default_cost("substation_design", _key, subkey="HVAC"), + ) self.topside_mass = 3.85 * self.mpt_rating * self.num_mpt + 285 self.topside_cost = ( @@ -208,8 +223,10 @@ def calc_shunt_reactor_cost(self): shunt_cost_rate : int | float """ - _design = self.config.get("substation_design", {}) - shunt_cost_rate = _design.get("shunt_cost_rate", 35000) + _key = "shunt_cost_rate" + shunt_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) self.shunt_reactor_cost = ( self.mpt_rating * self.num_mpt * shunt_cost_rate * 0.5 @@ -224,10 +241,12 @@ def calc_switchgear_cost(self): switchgear_cost : int | float """ - _design = self.config.get("substation_design", {}) - switchgear_cost = _design.get("switchgear_cost", 4e6) + _key = "switchgear_cost" + switchgear_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) - self.switchgear_costs = self.num_mpt * switchgear_cost + self.switchgear_costs = self.num_mpt * switchgear_cost_rate def calc_ancillary_system_cost(self): """ @@ -240,10 +259,20 @@ def calc_ancillary_system_cost(self): other_ancillary_cost : int | float """ - _design = self.config.get("substation_design", {}) - backup_gen_cost = _design.get("backup_gen_cost", 1e6) - workspace_cost = _design.get("workspace_cost", 2e6) - other_ancillary_cost = _design.get("other_ancillary_cost", 3e6) + _key = "backup_gen_cost" + backup_gen_cost = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) + + _key = "workspace_cost" + workspace_cost = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) + + _key = "other_ancillary_cost" + other_ancillary_cost = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) self.ancillary_system_costs = ( backup_gen_cost + workspace_cost + other_ancillary_cost @@ -258,8 +287,11 @@ def calc_assembly_cost(self): topside_assembly_factor : int | float """ - _design = self.config.get("substation_design", {}) - topside_assembly_factor = _design.get("topside_assembly_factor", 0.075) + _key = "topside_assembly_factor" + topside_assembly_factor = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) + self.land_assembly_cost = ( self.switchgear_costs + self.shunt_reactor_cost @@ -276,11 +308,15 @@ def calc_substructure_mass_and_cost(self): oss_pile_cost_rate : int | float """ - _design = self.config.get("substation_design", {}) - oss_substructure_cost_rate = _design.get( - "oss_substructure_cost_rate", 3000 + _key = "oss_substructure_cost_rate" + oss_substructure_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) + ) + + _key = "oss_pile_cost_rate" + oss_pile_cost_rate = self._design.get( + _key, self.get_default_cost("substation_design", _key) ) - oss_pile_cost_rate = _design.get("oss_pile_cost_rate", 0) substructure_mass = 0.4 * self.topside_mass substructure_pile_mass = 8 * substructure_mass**0.5574 diff --git a/ORBIT/phases/design/semi_submersible_design.py b/ORBIT/phases/design/semi_submersible_design.py index 23f0fd0c..17fd0a80 100644 --- a/ORBIT/phases/design/semi_submersible_design.py +++ b/ORBIT/phases/design/semi_submersible_design.py @@ -1,13 +1,17 @@ -"""Provides the `SemiSubmersibleDesign` class (from OffshoreBOS).""" +"""Provides the `SemiSubmersibleDesign` class.""" __author__ = "Jake Nunemaker" __copyright__ = "Copyright 2020, National Renewable Energy Laboratory" __maintainer__ = "Jake Nunemaker" __email__ = "jake.nunemaker@nrel.gov" - from ORBIT.phases.design import DesignPhase +""" +[1] Maness et al. 2017, NREL Offshore Balance-of-System Model. +https://www.nrel.gov/docs/fy17osti/66874.pdf +""" + class SemiSubmersibleDesign(DesignPhase): """Semi-Submersible Substructure Design.""" @@ -61,90 +65,98 @@ def run(self): @property def stiffened_column_mass(self): - """ - Calculates the mass of the stiffened column for a single - semi-submersible in tonnes. From original OffshoreBOS model. + """Calculates the mass of the stiffened column for a single + semi-submersible in tonnes [1]. """ rating = self.config["turbine"]["turbine_rating"] + mass = -0.9581 * rating**2 + 40.89 * rating + 802.09 return mass @property def stiffened_column_cost(self): - """ - Calculates the cost of the stiffened column for a single - semi-submersible. From original OffshoreBOS model. + """Calculates the cost of the stiffened column for a single + semi-submersible [1]. """ - cr = self._design.get("stiffened_column_CR", 3120) + _key = "stiffened_column_CR" + cr = self._design.get( + _key, self.get_default_cost("semisubmersible_design", _key) + ) return self.stiffened_column_mass * cr @property def truss_mass(self): - """ - Calculates the truss mass for a single semi-submersible in tonnes. From - original OffshoreBOS model. + """Calculates the truss mass for a single semi-submersible in tonnes + [1]. """ rating = self.config["turbine"]["turbine_rating"] + mass = 2.7894 * rating**2 + 15.591 * rating + 266.03 return mass @property def truss_cost(self): - """ - Calculates the cost of the truss for a signle semi-submerisble. From - original OffshoreBOS model. + """Calculates the cost of the truss for a signle semi-submerisble + [1]. """ - cr = self._design.get("truss_CR", 6250) + _key = "truss_CR" + cr = self._design.get( + _key, self.get_default_cost("semisubmersible_design", _key) + ) return self.truss_mass * cr @property def heave_plate_mass(self): - """ - Calculates the heave plate mass for a single semi-submersible in - tonnes. Source: original OffshoreBOS model. + """Calculates the heave plate mass for a single semi-submersible + in tonnes [1]. """ rating = self.config["turbine"]["turbine_rating"] + mass = -0.4397 * rating**2 + 21.545 * rating + 177.42 return mass @property def heave_plate_cost(self): - """ - Calculates the heave plate cost for a single semi-submersible. From - original OffshoreBOS model. + """Calculates the heave plate cost for a single semi-submersible + [1]. """ - cr = self._design.get("heave_plate_CR", 6250) + _key = "heave_plate_CR" + cr = self._design.get( + _key, self.get_default_cost("semisubmersible_design", _key) + ) return self.heave_plate_mass * cr @property def secondary_steel_mass(self): - """ - Calculates the mass of the required secondary steel for a single - semi-submersible. From original OffshoreBOS model. + """Calculates the mass of the required secondary steel for a single + semi-submersible [1]. """ rating = self.config["turbine"]["turbine_rating"] + mass = -0.153 * rating**2 + 6.54 * rating + 128.34 return mass @property def secondary_steel_cost(self): - """ - Calculates the cost of the required secondary steel for a single - semi-submersible. For original OffshoreBOS model. + """Calculates the cost of the required secondary steel for a single + semi-submersible [1]. """ - cr = self._design.get("secondary_steel_CR", 7250) + _key = "secondary_steel_CR" + cr = self._design.get( + _key, self.get_default_cost("semisubmersible_design", _key) + ) return self.secondary_steel_mass * cr @property diff --git a/ORBIT/phases/design/spar_design.py b/ORBIT/phases/design/spar_design.py index 151e12b3..e3713c4c 100644 --- a/ORBIT/phases/design/spar_design.py +++ b/ORBIT/phases/design/spar_design.py @@ -1,4 +1,4 @@ -"""Provides the `SparDesign` class (from OffshoreBOS).""" +"""Provides the `SparDesign` class.""" __author__ = "Jake Nunemaker" __copyright__ = "Copyright 2020, National Renewable Energy Laboratory" @@ -10,6 +10,11 @@ from ORBIT.phases.design import DesignPhase +""" +[1] Maness et al. 2017, NREL Offshore Balance-of-System Model. +https://www.nrel.gov/docs/fy17osti/66874.pdf +""" + class SparDesign(DesignPhase): """Spar Substructure Design.""" @@ -65,9 +70,8 @@ def run(self): @property def stiffened_column_mass(self): - """ - Calculates the mass of the stiffened column for a single spar in - tonnes. Source: original OffshoreBOS model. + """Calculates the mass of the stiffened column for a single spar + in tonnes [1]. """ rating = self.config["turbine"]["turbine_rating"] @@ -79,9 +83,8 @@ def stiffened_column_mass(self): @property def tapered_column_mass(self): - """ - Calculates the mass of the atpered column for a single spar in tonnes. - Source: original OffshoreBOS model. + """Calculates the mass of the tapered column for a single + spar in tonnes [1]. """ rating = self.config["turbine"]["turbine_rating"] @@ -92,51 +95,47 @@ def tapered_column_mass(self): @property def stiffened_column_cost(self): + """Calculates the cost of the stiffened column for a single spar + [1]. """ - Calculates the cost of the stiffened column for a single spar. - Source: original OffshoreBOS model. - """ - cr = self._design.get("stiffened_column_CR", 3120) + _key = "stiffened_column_CR" + cr = self._design.get(_key, self.get_default_cost("spar_design", _key)) + return self.stiffened_column_mass * cr @property def tapered_column_cost(self): - """ - Calculates the cost of the tapered column for a single spar. - Source: original OffshoreBOS model. - """ + """Calculates the cost of the tapered column for a single spar [1].""" + + _key = "tapered_column_CR" + cr = self._design.get(_key, self.get_default_cost("spar_design", _key)) - cr = self._design.get("tapered_column_CR", 4220) return self.tapered_column_mass * cr @property def ballast_mass(self): - """ - Calculates the ballast mass of a single spar. - Source: original OffshoreBOS model. - """ + """Calculates the ballast mass of a single spar [1].""" rating = self.config["turbine"]["turbine_rating"] + mass = -16.536 * rating**2 + 1261.8 * rating - 1554.6 return mass @property def ballast_cost(self): - """ - Calculates the cost of ballast material for a single spar. - Source: original OffshoreBOS model. - """ + """Calculates the cost of ballast material for a single spar [1].""" + + _key = "ballast_material_CR" + cr = self._design.get(_key, self.get_default_cost("spar_design", _key)) - cr = self._design.get("ballast_material_CR", 100) return self.ballast_mass * cr @property def secondary_steel_mass(self): - """ - Calculates the mass of the required secondary steel for a single - spar. From original OffshoreBOS model. + """Calculates the mass of the required secondary steel for a single + spar [1]. """ rating = self.config["turbine"]["turbine_rating"] @@ -152,12 +151,13 @@ def secondary_steel_mass(self): @property def secondary_steel_cost(self): + """Calculates the cost of the required secondary steel for a single + spar [1]. """ - Calculates the cost of the required secondary steel for a single - spar. For original OffshoreBOS model. - """ - cr = self._design.get("secondary_steel_CR", 7250) + _key = "secondary_steel_CR" + cr = self._design.get(_key, self.get_default_cost("spar_design", _key)) + return self.secondary_steel_mass * cr @property @@ -178,7 +178,10 @@ def ballasted_mass(self): @property def substructure_cost(self): - """Returns the cost (including ballast) of the spar substructure.""" + """ + Returns the total cost (including ballast) of the spar + substructure. + """ return ( self.stiffened_column_cost diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 92f5b249..6349519e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -5,6 +5,7 @@ ORBIT Changelog Unreleased (TBD) ---------------- +- Relocated all the get design costs in each design class to `common_cost.yaml`. Spar, Semisub, monopile, electrical, offshore substation, and mooring designs all recieved this update. Merged SemiTaut Moorings ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/phases/design/test_electrical_design.py b/tests/phases/design/test_electrical_design.py index fb4e3838..2854b934 100644 --- a/tests/phases/design/test_electrical_design.py +++ b/tests/phases/design/test_electrical_design.py @@ -68,9 +68,8 @@ def test_parameter_sweep(distance_to_landfall, depth, plant_cap, cable): def test_detailed_design_length(): - """ - Ensure that the same # of output variables are used for a floating and - fixed offshore substation. + """Ensure that the same # of output variables are used for a floating + and fixed offshore substation. """ elect = ElectricalDesign(base) @@ -105,31 +104,56 @@ def test_calc_substructure_mass_and_cost(): def test_calc_topside_mass_and_cost(): - """ - Test topside mass and cost for HVDC compared to HVDC-Monopole and - HVDC-Bipole. - """ + # Test topside mass and cost for HVDC compared to HVDC-Monopole and + # HVDC-Bipole. + # - elect = ElectricalDesign(base) + config = deepcopy(base) + config["substation_design"]["topside_design_cost"] = 9999.9 + elect = ElectricalDesign(config) elect.run() - base_dc = deepcopy(base) - cables = ["HVDC_2000mm_320kV", "HVDC_2500mm_525kV"] + assert elect._outputs["num_substations"] == 1 + assert elect._outputs["offshore_substation_topside"][ + "unit_cost" + ] == pytest.approx(23683541, abs=1e2) - for cable in cables: - base_dc["export_system_design"]["cables"] = cable + mono_dc = deepcopy(base) + mono_dc["export_system_design"]["cables"] = "HVDC_2000mm_320kV" + elect_mono = ElectricalDesign(mono_dc) + elect_mono.run() - elect_dc = ElectricalDesign(base_dc) - elect_dc.run() + assert ( + elect.detailed_output["substation_topside_mass"] + == elect_mono.detailed_output["substation_topside_mass"] + ) + assert ( + elect.detailed_output["substation_topside_cost"] + != elect_mono.detailed_output["substation_topside_cost"] + ) - assert ( - elect.detailed_output["substation_topside_mass"] - == elect_dc.detailed_output["substation_topside_mass"] - ) - assert ( - elect.detailed_output["substation_topside_cost"] - != elect_dc.detailed_output["substation_topside_cost"] - ) + bi_dc = deepcopy(base) + bi_dc["export_system_design"]["cables"] = "HVDC_2500mm_525kV" + elect_bi = ElectricalDesign(bi_dc) + elect_bi.run() + + assert ( + elect.detailed_output["substation_topside_mass"] + == elect_bi.detailed_output["substation_topside_mass"] + ) + assert ( + elect.detailed_output["substation_topside_cost"] + != elect_bi.detailed_output["substation_topside_cost"] + ) + + assert ( + elect_bi.detailed_output["substation_topside_mass"] + == elect_mono.detailed_output["substation_topside_mass"] + ) + assert ( + elect_bi.detailed_output["substation_topside_cost"] + != elect_mono.detailed_output["substation_topside_cost"] + ) def test_oss_substructure_kwargs(): @@ -220,6 +244,8 @@ def test_new_old_hvac_substation(): config["plant"]["num_turbines"] = 200 config["turbine"] = {"turbine_rating": 5} + config["export_system"] = {"cable": {"number": 5, "cable_type": "HVAC"}} + new = ElectricalDesign(config) new.run() @@ -266,18 +292,21 @@ def test_onshore_substation(): config = deepcopy(base) elect = ElectricalDesign(config) elect.run() + assert elect.onshore_compensation_cost != 0.0 assert elect.onshore_cost == pytest.approx(95.487e6, abs=1e2) # 109.32e6 config_mono = deepcopy(config) config_mono["export_system_design"] = {"cables": "HVDC_2000mm_320kV"} o_monelect = ElectricalDesign(config_mono) o_monelect.run() + assert o_monelect.onshore_compensation_cost == 0.0 assert o_monelect.onshore_cost == 244.3e6 config_bi = deepcopy(config) config_bi["export_system_design"] = {"cables": "HVDC_2500mm_525kV"} o_bi = ElectricalDesign(config_bi) o_bi.run() + assert o_bi.onshore_compensation_cost == 0.0 assert o_bi.onshore_cost == 450e6 diff --git a/tests/phases/design/test_oss_design.py b/tests/phases/design/test_oss_design.py index b6257e2d..9f3c0ba5 100644 --- a/tests/phases/design/test_oss_design.py +++ b/tests/phases/design/test_oss_design.py @@ -16,6 +16,7 @@ "plant": {"num_turbines": 50}, "turbine": {"turbine_rating": 6}, "substation_design": {}, + "export_system": {"cable": {"number": 3, "cable_type": "HVAC"}}, } @@ -30,6 +31,7 @@ def test_parameter_sweep(depth, num_turbines, turbine_rating): "plant": {"num_turbines": num_turbines}, "turbine": {"turbine_rating": turbine_rating}, "substation_design": {}, + "export_system": {"cable": {"number": 3, "cable_type": "HVAC"}}, } o = OffshoreSubstationDesign(config)