diff --git a/ORBIT/core/__init__.py b/ORBIT/core/__init__.py index 4d8e9ab6..a714c39b 100644 --- a/ORBIT/core/__init__.py +++ b/ORBIT/core/__init__.py @@ -6,7 +6,7 @@ __email__ = "jake.nunemaker@nrel.gov" -from .port import Port +from .port import Port, WetStorage from .cargo import Cargo from .vessel import Vessel from .components import Crane, JackingSys diff --git a/ORBIT/core/_defaults.py b/ORBIT/core/_defaults.py index 3b0470ea..40ddd6e8 100755 --- a/ORBIT/core/_defaults.py +++ b/ORBIT/core/_defaults.py @@ -53,6 +53,11 @@ "blade_fasten_time": 1.5, # hr "blade_release_time": 1, # hr "blade_attach_time": 3.5, # hr + # Mooring System + "mooring_system_load_time": 5, # hr + "mooring_site_survey_time": 4, # hr + "suction_pile_install_time": 11, # hr + "drag_embed_install_time": 5, # hr # Misc. "site_position_time": 2, # hr "rov_survey_time": 1, # hr diff --git a/ORBIT/core/logic/vessel_logic.py b/ORBIT/core/logic/vessel_logic.py index 8021d966..1e7c8c1d 100644 --- a/ORBIT/core/logic/vessel_logic.py +++ b/ORBIT/core/logic/vessel_logic.py @@ -228,12 +228,14 @@ def get_list_of_items_from_port(vessel, port, items, **kwargs): for item in buffer: action, time = item.fasten(**kwargs) vessel.storage.put_item(item) - yield vessel.task( - action, - time, - constraints=vessel.transit_limits, - **kwargs, - ) + + if time > 0: + yield vessel.task( + action, + time, + constraints=vessel.transit_limits, + **kwargs, + ) else: raise ItemNotFound(items) diff --git a/ORBIT/core/port.py b/ORBIT/core/port.py index e2b9fc53..dbfc152a 100644 --- a/ORBIT/core/port.py +++ b/ORBIT/core/port.py @@ -63,3 +63,19 @@ def get_item(self, _type): else: res = self.get(lambda x: x == target) return res.value + + +class WetStorage(simpy.Store): + """Storage infrastructure for floating substructures.""" + + def __init__(self, env, capacity): + """ + Creates an instance of WetStorage. + + Parameters + ---------- + capacity : int + Number of substructures or assemblies that can be stored. + """ + + super().__init__(env, capacity) diff --git a/ORBIT/core/vessel.py b/ORBIT/core/vessel.py index 5e2651dc..74c6fdf6 100644 --- a/ORBIT/core/vessel.py +++ b/ORBIT/core/vessel.py @@ -269,12 +269,13 @@ def get_item_from_storage( raise e action, time = item.release(**kwargs) - yield self.task( - action, - time, - constraints=self.transit_limits, - cost=self.operation_cost(time), - ) + if time > 0: + yield self.task( + action, + time, + constraints=self.transit_limits, + cost=self.operation_cost(time), + ) if release and vessel.storage.any_remaining(_type) is False: vessel.release.succeed() diff --git a/ORBIT/library.py b/ORBIT/library.py index 8807a011..599c5a0f 100755 --- a/ORBIT/library.py +++ b/ORBIT/library.py @@ -273,7 +273,10 @@ def export_library_specs(key, filename, data, file_ext="yaml"): "spi_vessel": "vessels", "trench_dig_vessel": "vessels", "feeder": "vessels", + "mooring_install_vessel": "vessels", "wtiv": "vessels", + "towing_vessel": "vessels", + "support_vessel": "vessels", # cables "cables": "cables", "array_system": "cables", diff --git a/ORBIT/manager.py b/ORBIT/manager.py index 390da9d0..2bf59a55 100644 --- a/ORBIT/manager.py +++ b/ORBIT/manager.py @@ -19,19 +19,25 @@ from ORBIT.phases import DesignPhase, InstallPhase from ORBIT.library import initialize_library, extract_library_data from ORBIT.phases.design import ( + SparDesign, MonopileDesign, ArraySystemDesign, ExportSystemDesign, ProjectDevelopment, + MooringSystemDesign, ScourProtectionDesign, + SemiSubmersibleDesign, CustomArraySystemDesign, OffshoreSubstationDesign, ) from ORBIT.phases.install import ( TurbineInstallation, MonopileInstallation, + MooredSubInstallation, ArrayCableInstallation, ExportCableInstallation, + GravityBasedInstallation, + MooringSystemInstallation, ScourProtectionInstallation, OffshoreSubstationInstallation, ) @@ -56,6 +62,9 @@ class ProjectManager: ExportSystemDesign, ScourProtectionDesign, OffshoreSubstationDesign, + MooringSystemDesign, + SemiSubmersibleDesign, + SparDesign, ] _install_phases = [ @@ -65,6 +74,9 @@ class ProjectManager: ArrayCableInstallation, ExportCableInstallation, ScourProtectionInstallation, + MooredSubInstallation, + MooringSystemInstallation, + GravityBasedInstallation, ] def __init__(self, config, library_path=None, weather=None): diff --git a/ORBIT/phases/design/__init__.py b/ORBIT/phases/design/__init__.py index 15cccc58..c7d85801 100644 --- a/ORBIT/phases/design/__init__.py +++ b/ORBIT/phases/design/__init__.py @@ -8,8 +8,11 @@ from .design_phase import DesignPhase # isort:skip from .oss_design import OffshoreSubstationDesign +from .spar_design import SparDesign from .monopile_design import MonopileDesign from .array_system_design import ArraySystemDesign, CustomArraySystemDesign from .project_development import ProjectDevelopment from .export_system_design import ExportSystemDesign +from .mooring_system_design import MooringSystemDesign from .scour_protection_design import ScourProtectionDesign +from .semi_submersible_design import SemiSubmersibleDesign diff --git a/ORBIT/phases/design/_cables.py b/ORBIT/phases/design/_cables.py index e81eeedf..9611ce6e 100755 --- a/ORBIT/phases/design/_cables.py +++ b/ORBIT/phases/design/_cables.py @@ -10,6 +10,7 @@ from collections import Counter, OrderedDict import numpy as np +from scipy.optimize import fsolve from ORBIT.library import extract_library_specs from ORBIT.phases.design import DesignPhase @@ -310,6 +311,72 @@ def _initialize_cables(self): name = specs["name"] self.cables[name] = Cable(specs) + def _get_touchdown_distance(self): + """ + Returns the cable touchdown distance measured from the centerpoint of + the substructure. + + If depth <= 60, default is 0km (straight down assumed for fixed bottom). + If depth > 60, default is 0.3 * depth. + """ + + _design = f"{self.cable_type}_system_design" + depth = self.config["site"]["depth"] + touchdown = self.config[_design].get("touchdown_distance", None) + + if touchdown is not None: + self.touchdown = touchdown + + else: + if depth <= 60: + self.touchdown = 0 + + else: + self.touchdown = depth * 0.3 + + @staticmethod + def _catenary(a, *data): + """Simple catenary equation.""" + + d, h = data + res = a * np.cosh(h / a) - (d + a) + return res + + def _get_catenary_length(self, d, h): + """ + Returns the catenary length of a cable that touches down at depth `d` + and horizontal distance `h`. + + Returns + ------- + float + Catenary length. + """ + + a = fsolve(self._catenary, 8, (d, h)) + + x = np.linspace(0, h) + y = a * np.cosh(x / a) - a + + if not np.isclose(y[-1], d): + print( + "Warning: Catenary calculation failed. Reverting to simple vertical profile." + ) + return d + + return np.trapz(np.sqrt(1 + np.gradient(y, x) ** 2), x) + + @property + def free_cable_length(self): + """Returns the length of the vertical portion of a cable section in km.""" + + depth = self.config["site"]["depth"] + + if not self.touchdown: + return depth / 1000 + + return self._get_catenary_length(depth, self.touchdown) / 1000 + @property def cable_lengths_by_type(self): """ diff --git a/ORBIT/phases/design/array_system_design.py b/ORBIT/phases/design/array_system_design.py index d9b1887d..c8904ff6 100644 --- a/ORBIT/phases/design/array_system_design.py +++ b/ORBIT/phases/design/array_system_design.py @@ -79,6 +79,7 @@ class ArraySystemDesign(CableSystem): "array_system_design": { "design_time": "hrs (optional)", "cables": "list | str", + "touchdown_distance": "m (optional, default: 0)", "average_exclusion_percent": "float (optional)", }, } @@ -102,6 +103,7 @@ def __init__(self, config, **kwargs): self.exclusion = 1 + self.config["array_system_design"].get( "average_exclusion_percent", 0.0 ) + self._get_touchdown_distance() self.extract_phase_kwargs(**kwargs) self.system = Plant(self.config) @@ -331,7 +333,8 @@ def _create_cable_section_lengths(self): if getattr(self, "sections_cable_lengths", np.zeros(1)).sum() == 0: self.sections_cable_lengths = ( self.sections_distance * self.exclusion - + (2 * self.system.site_depth) + + (2 * self.free_cable_length) + - (2 * self.touchdown / 1000) ) self.sections_cables = np.full( (self.num_strings, self.num_turbines_full_string), None diff --git a/ORBIT/phases/design/export_system_design.py b/ORBIT/phases/design/export_system_design.py index 91bee5ec..e415cb91 100755 --- a/ORBIT/phases/design/export_system_design.py +++ b/ORBIT/phases/design/export_system_design.py @@ -42,6 +42,7 @@ class ExportSystemDesign(CableSystem): "export_system_design": { "cables": "str", "num_redundant": "int (optional)", + "touchdown_distance": "m (optional, default: 0)", "percent_added_length": "float (optional)", }, } @@ -78,6 +79,7 @@ def __init__(self, config, **kwargs): self._depth = config["site"]["depth"] self._plant_capacity = self.config["plant"]["capacity"] self._distance_to_landfall = config["site"]["distance_to_landfall"] + self._get_touchdown_distance() try: self._distance_to_interconnection = config["landfall"][ "interconnection_distance" @@ -136,8 +138,8 @@ def compute_cable_length(self): added_length = 1.0 + self._design.get("percent_added_length", 0.0) self.length = round( ( - (self._depth / 1000.0) # convert to km - + self._distance_to_landfall + self.free_cable_length + + (self._distance_to_landfall - self.touchdown / 1000) + self._distance_to_interconnection ) * added_length, diff --git a/ORBIT/phases/design/mooring_system_design.py b/ORBIT/phases/design/mooring_system_design.py new file mode 100644 index 00000000..f14d0876 --- /dev/null +++ b/ORBIT/phases/design/mooring_system_design.py @@ -0,0 +1,193 @@ +"""`MooringSystemDesign` and related functionality.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from math import sqrt + +from ORBIT.phases.design import DesignPhase + + +class MooringSystemDesign(DesignPhase): + """Mooring System and Anchor Design.""" + + expected_config = { + "site": {"depth": "float"}, + "turbine": {"turbine_rating": "int | float"}, + "plant": {"num_turbines": "int"}, + "mooring_system_design": { + "num_lines": "int | float (optional, default: 4)", + "anchor_type": "str (optional, default: 'Suction Pile')", + "mooring_line_cost_rate": "int | float (optional)", + "drag_embedment_fixed_length": "int | float (optional, default: .5km)", + }, + } + + output_config = { + "mooring_system": { + "num_lines": "int", + "line_diam": "m, float", + "line_mass": "t", + "line_length": "m", + "anchor_mass": "t", + "anchor_type": "str", + } + } + + def __init__(self, config, **kwargs): + """ + Creates an instance of MooringSystemDesign. + + Parameters + ---------- + config : dict + """ + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.num_turbines = self.config["plant"]["num_turbines"] + + self._design = self.config.get("mooring_system_design", {}) + self.num_lines = self._design.get("num_lines", 4) + self.anchor_type = self._design.get("anchor_type", "Suction Pile") + + self.extract_defaults() + self._outputs = {} + + def run(self): + """ + Main run function. + """ + + self.determine_mooring_line() + self.calculate_breaking_load() + self.calculate_line_length_mass() + self.calculate_anchor_mass_cost() + + self._outputs["mooring_system"] = {**self.design_result} + + def determine_mooring_line(self): + """ + Returns the diameter of the mooring lines based on the turbine rating. + """ + + tr = self.config["turbine"]["turbine_rating"] + fit = -0.0004 * (tr ** 2) + 0.0132 * tr + 0.0536 + + if fit <= 0.09: + self.line_diam = 0.09 + self.line_mass_per_m = 0.161 + self.line_cost_rate = 399.0 + + elif fit <= 0.12: + self.line_diam = 0.12 + self.line_mass_per_m = 0.288 + self.line_cost_rate = 721.0 + + else: + self.line_diam = 0.15 + self.line_mass_per_m = 0.450 + self.line_cost_rate = 1088.0 + + def calculate_breaking_load(self): + """ + Returns the mooring line breaking load. + """ + + self.breaking_load = ( + 419449 * (self.line_diam ** 2) + 93415 * self.line_diam - 3577.9 + ) + + def calculate_line_length_mass(self): + """ + Returns the mooring line length and mass. + """ + + if self.anchor_type == "Drag Embedment": + fixed = self._design.get("drag_embedment_fixed_length", 0.5) + + else: + fixed = 0 + + depth = self.config["site"]["depth"] + self.line_length = ( + 0.0002 * (depth ** 2) + 1.264 * depth + 47.776 + fixed + ) + + self.line_mass = self.line_length * self.line_mass_per_m + + 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. + """ + + if self.anchor_type == "Drag Embedment": + self.anchor_mass = 20 + self.anchor_cost = self.breaking_load / 9.81 / 20.0 * 2000.0 + + else: + self.anchor_mass = 50 + self.anchor_cost = sqrt(self.breaking_load / 9.81 / 1250) * 150000 + + def calculate_total_cost(self): + """ + Returns the total cost of the mooring system. + """ + + return ( + self.num_lines + * self.num_turbines + * (self.anchor_cost + self.line_length * self.line_cost_rate) + ) + + @property + def design_result(self): + """Returns the results of the design phase.""" + + return { + "mooring_system": { + "num_lines": self.num_lines, + "line_diam": self.line_diam, + "line_mass": self.line_mass, + "line_length": self.line_length, + "anchor_mass": self.anchor_mass, + "anchor_type": self.anchor_type, + } + } + + @property + def total_phase_cost(self): + """Returns total phase cost in $USD.""" + + _design = self.config.get("mooring_system_design", {}) + design_cost = _design.get("design_cost", 0.0) + return self.calculate_total_cost() + design_cost + + @property + def total_phase_time(self): + """Returns total phase time in hours.""" + + _design = self.config.get("mooring_system_design", {}) + phase_time = _design.get("design_time", 0.0) + return phase_time + + @property + def detailed_output(self): + """Returns detailed phase information.""" + + return { + "num_lines": self.num_lines, + "line_diam": self.line_diam, + "line_mass": self.line_mass, + "line_length": self.line_length, + "anchor_type": self.anchor_type, + "anchor_mass": self.anchor_mass, + "anchor_cost": self.anchor_cost, + "system_cost": self.calculate_total_cost(), + } diff --git a/ORBIT/phases/design/semi_submersible_design.py b/ORBIT/phases/design/semi_submersible_design.py new file mode 100644 index 00000000..74392e8e --- /dev/null +++ b/ORBIT/phases/design/semi_submersible_design.py @@ -0,0 +1,224 @@ +"""Provides the `SemiSubmersibleDesign` class (from OffshoreBOS).""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from ORBIT.phases.design import DesignPhase + + +class SemiSubmersibleDesign(DesignPhase): + """Semi-Submersible Substructure Design""" + + expected_config = { + "site": {"depth": "m"}, + "plant": {"num_turbines": "int"}, + "turbine": {"turbine_rating": "MW"}, + "semisubmersible_design": { + "stiffened_column_CR": "$/t (optional, default: 3120)", + "truss_CR": "$/t (optional, default: 6250)", + "heave_plate_CR": "$/t (optional, default: 6250)", + "secondary_steel_CR": "$/t (optional, default: 7250)", + "towing_speed": "km/h (optional, default: 6)", + "design_time": "h, (optional, default: 0)", + "design_cost": "h, (optional, default: 0)", + }, + } + + output_config = {} + + def __init__(self, config, **kwargs): + """ + Creates an instance of `SemiSubmersibleDesign`. + + Parameters + ---------- + config : dict + """ + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.extract_defaults() + self._design = self.config.get("semisubmersible_design", {}) + + self._outputs = {} + + def run(self): + """Main run function.""" + + substructure = { + "mass": self.substructure_mass, + "cost": self.substructure_cost, + "towing_speed": self._design.get("towing_speed", 6), + } + + self._outputs["semisubmersible"] = substructure + + @property + def stiffened_column_mass(self): + """ + Calculates the mass of the stiffened column for a single + semi-submersible in tonnes. From original OffshoreBOS model. + """ + + 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. + """ + + cr = self._design.get("stiffened_column_CR", 3120) + 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. + """ + + 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. + """ + + cr = self._design.get("truss_CR", 6250) + return self.truss_mass * cr + + @property + def heave_plate_mass(self): + """ + Calculates the heave plate mass for a single semi-submersible in tonnes. + From original OffshoreBOS model. + """ + + 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. + """ + + cr = self._design.get("heave_plate_CR", 6250) + 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. + """ + + 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. + """ + + cr = self._design.get("secondary_steel_CR", 7250) + return self.secondary_steel_mass * cr + + @property + def substructure_mass(self): + """Returns single substructure mass.""" + + return ( + self.stiffened_column_mass + + self.truss_mass + + self.heave_plate_mass + + self.secondary_steel_mass + ) + + @property + def substructure_cost(self): + """Returns single substructure cost.""" + + return ( + self.stiffened_column_cost + + self.truss_cost + + self.heave_plate_cost + + self.secondary_steel_cost + ) + + @property + def total_substructure_mass(self): + """Returns mass of all substructures.""" + + num = self.config["plant"]["num_turbines"] + return num * self.substructure_mass + + @property + def total_substructure_cost(self): + """Retruns cost of all substructures.""" + + num = self.config["plant"]["num_turbines"] + return num * self.substructure_cost + + @property + def design_result(self): + """Returns the result of `self.run()`""" + + if not self._outputs: + raise Exception("Has `SemiSubmersibleDesign` been ran yet?") + + return self._outputs + + @property + def total_phase_cost(self): + """Returns total phase cost in $USD.""" + + _design = self.config.get("semisubmersible_design", {}) + design_cost = _design.get("design_cost", 0.0) + + return design_cost + self.total_substructure_cost + + @property + def total_phase_time(self): + """Returns total phase time in hours.""" + + _design = self.config.get("semisubmersible_design", {}) + phase_time = _design.get("design_time", 0.0) + return phase_time + + @property + def detailed_output(self): + """Returns detailed phase information.""" + + _outputs = { + "stiffened_column_mass": self.stiffened_column_mass, + "stiffened_column_cost": self.stiffened_column_cost, + "truss_mass": self.truss_mass, + "truss_cost": self.truss_cost, + "heave_plate_mass": self.heave_plate_mass, + "heave_plate_cost": self.heave_plate_cost, + "secondary_steel_mass": self.secondary_steel_mass, + "secondary_steel_cost": self.secondary_steel_cost, + } + + return _outputs diff --git a/ORBIT/phases/design/spar_design.py b/ORBIT/phases/design/spar_design.py new file mode 100644 index 00000000..a5531161 --- /dev/null +++ b/ORBIT/phases/design/spar_design.py @@ -0,0 +1,228 @@ +"""Provides the `SparDesign` class (from OffshoreBOS).""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from numpy import exp, log + +from ORBIT.phases.design import DesignPhase + + +class SparDesign(DesignPhase): + """Spar Substructure Design""" + + expected_config = { + "site": {"depth": "m"}, + "plant": {"num_turbines": "int"}, + "turbine": {"turbine_rating": "MW"}, + "spar_design": { + "stiffened_column_CR": "$/t (optional, default: 3120)", + "tapered_column_CR": "$/t (optional, default: 4220)", + "ballast_material_CR": "$/t (optional, default: 100)", + "secondary_steel_CR": "$/t (optional, default: 7250)", + "towing_speed": "km/h (optional, default: 6)", + "design_time": "h, (optional, default: 0)", + "design_cost": "h, (optional, default: 0)", + }, + } + + output_config = {} + + def __init__(self, config, **kwargs): + """ + Creates an instance of `SparDesign`. + + Parameters + ---------- + config : dict + """ + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.extract_defaults() + self._design = self.config.get("spar_design", {}) + + self._outputs = {} + + def run(self): + """Main run function.""" + + substructure = { + "mass": self.unballasted_mass, + "ballasted_mass": self.ballasted_mass, + "cost": self.substructure_cost, + "towing_speed": self._design.get("towing_speed", 6), + } + + self._outputs["spar"] = substructure + + @property + def stiffened_column_mass(self): + """ + Calculates the mass of the stiffened column for a single spar in tonnes. From original OffshoreBOS model. + """ + + rating = self.config["turbine"]["turbine_rating"] + depth = self.config["site"]["depth"] + + mass = 535.93 + 17.664 * rating ** 2 + 0.02328 * depth * log(depth) + + return mass + + @property + def tapered_column_mass(self): + """ + Calculates the mass of the atpered column for a single spar in tonnes. From original OffshoreBOS model. + """ + + rating = self.config["turbine"]["turbine_rating"] + + mass = 125.81 * log(rating) + 58.712 + + return mass + + @property + def stiffened_column_cost(self): + """ + Calculates the cost of the stiffened column for a single spar. From original OffshoreBOS model. + """ + + cr = self._design.get("stiffened_column_CR", 3120) + return self.stiffened_column_mass * cr + + @property + def tapered_column_cost(self): + """ + Calculates the cost of the tapered column for a single spar. From original OffshoreBOS model. + """ + + 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. From original OffshoreBOS model. + """ + + 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. From original OffshoreBOS model. + """ + + 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. + """ + + rating = self.config["turbine"]["turbine_rating"] + depth = self.config["site"]["depth"] + + mass = exp( + 3.58 + + 0.196 * (rating ** 0.5) * log(rating) + + 0.00001 * depth * log(depth) + ) + + return mass + + @property + def secondary_steel_cost(self): + """ + Calculates the cost of the required secondary steel for a single + spar. For original OffshoreBOS model. + """ + + cr = self._design.get("secondary_steel_CR", 7250) + return self.secondary_steel_mass * cr + + @property + def unballasted_mass(self): + """Returns the unballasted mass of the spar substructure.""" + + return ( + self.stiffened_column_mass + + self.tapered_column_mass + + self.secondary_steel_mass + ) + + @property + def ballasted_mass(self): + """Returns the ballasted mass of the spar substructure.""" + + return self.unballasted_mass + self.ballast_mass + + @property + def substructure_cost(self): + """Returns the total cost (including ballast) of the spar substructure.""" + + return ( + self.stiffened_column_cost + + self.tapered_column_cost + + self.secondary_steel_cost + + self.ballast_cost + ) + + @property + def detailed_output(self): + """Returns detailed phase information.""" + + _outputs = { + "stiffened_column_mass": self.stiffened_column_mass, + "stiffened_column_cost": self.stiffened_column_cost, + "tapered_column_mass": self.tapered_column_mass, + "tapered_column_cost": self.tapered_column_cost, + "ballast_mass": self.ballast_mass, + "ballast_cost": self.ballast_cost, + "secondary_steel_mass": self.secondary_steel_mass, + "secondary_steel_cost": self.secondary_steel_cost, + } + + return _outputs + + @property + def total_substructure_cost(self): + """Retruns cost of all substructures.""" + + num = self.config["plant"]["num_turbines"] + return num * self.substructure_cost + + @property + def total_phase_cost(self): + """Returns total phase cost in $USD.""" + + _design = self.config.get("spar_design", {}) + design_cost = _design.get("design_cost", 0.0) + + return design_cost + self.total_substructure_cost + + @property + def total_phase_time(self): + """Returns total phase time in hours.""" + + _design = self.config.get("spar_design", {}) + phase_time = _design.get("design_time", 0.0) + return phase_time + + @property + def design_result(self): + """Returns the result of `self.run()`""" + + if not self._outputs: + raise Exception("Has `SparDesign` been ran yet?") + + return self._outputs diff --git a/ORBIT/phases/install/__init__.py b/ORBIT/phases/install/__init__.py index 121e069d..e584656c 100644 --- a/ORBIT/phases/install/__init__.py +++ b/ORBIT/phases/install/__init__.py @@ -8,6 +8,11 @@ from .install_phase import InstallPhase # isort:skip from .oss_install import OffshoreSubstationInstallation from .cable_install import ArrayCableInstallation, ExportCableInstallation +from .mooring_install import MooringSystemInstallation from .turbine_install import TurbineInstallation from .monopile_install import MonopileInstallation +from .quayside_assembly_tow import ( + MooredSubInstallation, + GravityBasedInstallation, +) from .scour_protection_install import ScourProtectionInstallation diff --git a/ORBIT/phases/install/cable_install/array.py b/ORBIT/phases/install/cable_install/array.py index 670eb233..a64f7f46 100755 --- a/ORBIT/phases/install/cable_install/array.py +++ b/ORBIT/phases/install/cable_install/array.py @@ -40,9 +40,10 @@ class ArrayCableInstallation(InstallPhase): "array_cable_install_vessel": "str", "array_cable_bury_vessel": "str (optional)", "array_cable_trench_vessel": "str (optional)", - "site": {"distance": "km"}, + "site": {"distance": "km", "depth": "m"}, "array_system": { "num_strings": "int (optional, default: 10)", + "free_cable_length": "km (optional, default: 'depth')", "cables": { "name (variable)": { "linear_density": "t/km", @@ -82,14 +83,18 @@ def setup_simulation(self, **kwargs): - """ + depth = self.config["site"]["depth"] + system = self.config["array_system"] + self.free_cable_length = system.get("free_cable_length", depth / 1000) + self.initialize_installation_vessel() self.initialize_burial_vessel() self.initialize_trench_vessel() - self.num_strings = self.config["array_system"].get("num_strings", 10) + self.num_strings = system.get("num_strings", 10) self.cable_data = [ (Cable(data["linear_density"]), deepcopy(data["cable_sections"])) - for _, data in self.config["array_system"]["cables"].items() + for _, data in system["cables"].items() ] # Perform cable installation @@ -100,6 +105,7 @@ def setup_simulation(self, **kwargs): num_strings=self.num_strings, burial_vessel=self.bury_vessel, trench_vessel=self.trench_vessel, + free_cable_length=self.free_cable_length, **kwargs, ) @@ -171,6 +177,7 @@ def install_array_cables( num_strings, burial_vessel=None, trench_vessel=None, + free_cable_length=None, **kwargs, ): """ @@ -197,14 +204,17 @@ def install_array_cables( breakpoints = list(np.linspace(1 / num_strings, 1, num_strings)) trench_sections = [] - total_cable_distance = 0 + total_cable_length = 0 installed = 0 for cable, sections in cable_data: for s in sections: - d_i, num_i, *_ = s - trench_sections.extend([d_i] * num_i) - total_cable_distance += d_i * num_i + l, num_i, *_ = s + total_cable_length += l * num_i + + _trench_length = max(0, l - 2 * free_cable_length) + if _trench_length: + trench_sections.extend([_trench_length] * num_i) ## Trenching Process # Conduct trenching along cable routes before laying cable @@ -299,7 +309,9 @@ def install_array_cables( else: yield lay_cable(vessel, section, **specs) - to_bury.append(section) + _bury = max(0, (section - 2 * free_cable_length)) + if _bury: + to_bury.append(_bury) # Post cable laying procedure (at substructure 2) yield prep_cable(vessel, **kwargs) @@ -308,10 +320,7 @@ def install_array_cables( if burial_vessel is None: breakpoints = check_for_completed_string( - vessel, - installed, - total_cable_distance, - breakpoints, + vessel, installed, total_cable_length, breakpoints ) # Transit back to port diff --git a/ORBIT/phases/install/cable_install/export.py b/ORBIT/phases/install/cable_install/export.py index 41850ac5..62dbcf26 100755 --- a/ORBIT/phases/install/cable_install/export.py +++ b/ORBIT/phases/install/cable_install/export.py @@ -85,7 +85,10 @@ def setup_simulation(self, **kwargs): - Routes to specific setup scripts based on configured strategy. """ + depth = self.config["site"]["depth"] system = self.config["export_system"] + self.free_cable_length = system.get("free_cable_length", depth / 1000) + self.cable = Cable(system["cable"]["linear_density"]) self.sections = system["cable"]["sections"] self.number = system["cable"].get("number", 1) @@ -95,7 +98,9 @@ def setup_simulation(self, **kwargs): self.initialize_trench_vessel() # Perform onshore construction - self.onshore_construction(**kwargs) + onshore = kwargs.get("include_onshore_construction", True) + if onshore: + self.onshore_construction(**kwargs) # Perform cable installation install_export_cables( @@ -106,6 +111,7 @@ def setup_simulation(self, **kwargs): distances=self.distances, burial_vessel=self.bury_vessel, trench_vessel=self.trench_vessel, + free_cable_length=self.free_cable_length, **kwargs, ) @@ -172,7 +178,9 @@ def calculate_onshore_transmission_cost(self, **kwargs): ) switchyard_cost = 18115 * voltage + 165944 - onshore_substation_cost = (0.165 * 1e6) * capacity # From BNEF Tomorrow's Cost of Offshore Wind + onshore_substation_cost = ( + 0.165 * 1e6 + ) * capacity # From BNEF Tomorrow's Cost of Offshore Wind onshore_misc_cost = 11795 * capacity ** 0.3549 + 350000 transmission_line_cost = (1176 * voltage + 218257) * ( distance ** (1 - 0.1063) @@ -253,6 +261,7 @@ def install_export_cables( distances, burial_vessel=None, trench_vessel=None, + free_cable_length=None, **kwargs, ): """ @@ -286,25 +295,33 @@ def install_export_cables( the cable lay vessel and digs a trench. """ + ground_distance = -free_cable_length + for s in sections: + try: + length, speed = s + + except TypeError: + length = s + + ground_distance += length + # Conduct trenching operations if trench_vessel is None: pass + else: for _ in range(number): - # Total export cable length along which to dig trench - total_sections_distance = sum(sections) - # Trenching vessel can dig a trench during inbound or outbound journey if trench_vessel.at_port: trench_vessel.at_port = False yield dig_export_cables_trench( - trench_vessel, total_sections_distance, **kwargs + trench_vessel, ground_distance, **kwargs ) trench_vessel.at_site = True elif trench_vessel.at_site: trench_vessel.at_site = False yield dig_export_cables_trench( - trench_vessel, total_sections_distance, **kwargs + trench_vessel, ground_distance, **kwargs ) trench_vessel.at_port = True @@ -312,7 +329,7 @@ def install_export_cables( # TODO: replace with demobilization method if trench_vessel.at_site: trench_vessel.at_site = False - yield trench_vessel.transit(total_sections_distance, **kwargs) + yield trench_vessel.transit(ground_distance, **kwargs) trench_vessel.at_port = True for _ in range(number): @@ -378,7 +395,7 @@ def install_export_cables( else: vessel.submit_debug_log(message="Export cable lay process completed!") - bury_export_cables(burial_vessel, length, number, **kwargs) + bury_export_cables(burial_vessel, ground_distance, number, **kwargs) @process diff --git a/ORBIT/phases/install/install_phase.py b/ORBIT/phases/install/install_phase.py index 0be267a2..3f200dcd 100644 --- a/ORBIT/phases/install/install_phase.py +++ b/ORBIT/phases/install/install_phase.py @@ -61,13 +61,14 @@ def initialize_port(self): Initializes a Port object with N number of cranes. """ + self.port = Port(self.env) + try: cranes = self.config["port"]["num_cranes"] - self.port = Port(self.env) self.port.crane = simpy.Resource(self.env, cranes) except KeyError: - self.port = Port(self.env) + self.port.crane = simpy.Resource(self.env, 1) def run(self, until=None): """ @@ -101,7 +102,8 @@ def port_costs(self): else: key = "port_cost_per_month" - rate = self.config["port"].get("monthly_rate", self.defaults[key]) + port_config = self.config.get("port", {}) + rate = port_config.get("monthly_rate", self.defaults[key]) months = self.total_phase_time / (8760 / 12) return months * rate diff --git a/ORBIT/phases/install/monopile_install/standard.py b/ORBIT/phases/install/monopile_install/standard.py index a8fcfae9..01e438f8 100644 --- a/ORBIT/phases/install/monopile_install/standard.py +++ b/ORBIT/phases/install/monopile_install/standard.py @@ -47,7 +47,7 @@ class MonopileInstallation(InstallPhase): "plant": {"num_turbines": "int"}, "turbine": {"hub_height": "m"}, "port": { - "num_cranes": "int", + "num_cranes": "int (optional, default: 1)", "monthly_rate": "USD/mo (optional)", "name": "str (optional)", }, diff --git a/ORBIT/phases/install/mooring_install/__init__.py b/ORBIT/phases/install/mooring_install/__init__.py new file mode 100644 index 00000000..6e6af116 --- /dev/null +++ b/ORBIT/phases/install/mooring_install/__init__.py @@ -0,0 +1,9 @@ +"""Mooring Installation Modules.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from .mooring import MooringSystemInstallation diff --git a/ORBIT/phases/install/mooring_install/mooring.py b/ORBIT/phases/install/mooring_install/mooring.py new file mode 100644 index 00000000..e0d3f4f0 --- /dev/null +++ b/ORBIT/phases/install/mooring_install/mooring.py @@ -0,0 +1,333 @@ +"""Installation strategies for mooring systems.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from marmot import process + +from ORBIT.core import Cargo, Vessel +from ORBIT.core.logic import position_onsite, get_list_of_items_from_port +from ORBIT.core._defaults import process_times as pt +from ORBIT.phases.install import InstallPhase +from ORBIT.core.exceptions import ItemNotFound + + +class MooringSystemInstallation(InstallPhase): + """Module to model the installation of mooring systems at sea.""" + + phase = "Mooring System Installation" + + #: + expected_config = { + "mooring_install_vessel": "dict | str", + "site": {"depth": "m", "distance": "km"}, + "plant": {"num_turbines": "int"}, + "mooring_system": { + "num_lines": "int", + "line_mass": "t", + "anchor_mass": "t", + "anchor_type": "str (optional, default: 'Suction Pile')", + }, + } + + def __init__(self, config, weather=None, **kwargs): + """ + Creates an instance of `MooringSystemInstallation`. + + Parameters + ---------- + config : dict + Simulation specific configuration. + weather : np.array + Weather data at site. + """ + + super().__init__(weather, **kwargs) + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.extract_defaults() + + self.setup_simulation(**kwargs) + + def setup_simulation(self, **kwargs): + """ + Sets up the required simulation infrastructure: + - initializes port + - initializes installation vessel + - initializes mooring systems at port. + """ + + self.initialize_port() + self.initialize_installation_vessel() + self.initialize_components() + + depth = self.config["site"]["depth"] + distance = self.config["site"]["distance"] + + install_mooring_systems( + self.vessel, + self.port, + distance, + depth, + self.number_systems, + **kwargs, + ) + + def initialize_installation_vessel(self): + """Initializes the mooring system installation vessel.""" + + vessel_specs = self.config.get("mooring_install_vessel", None) + name = vessel_specs.get("name", "Mooring System Installation Vessel") + + vessel = Vessel(name, vessel_specs) + self.env.register(vessel) + + vessel.initialize() + vessel.at_port = True + vessel.at_site = False + self.vessel = vessel + + def initialize_components(self): + """Initializes the Cargo components at port.""" + + system = MooringSystem(**self.config["mooring_system"]) + self.number_systems = self.config["plant"]["num_turbines"] + + for _ in range(self.number_systems): + self.port.put(system) + + @property + def detailed_output(self): + """Detailed outputs of the scour protection installation.""" + + outputs = {self.phase: {**self.agent_efficiencies}} + + return outputs + + +@process +def install_mooring_systems(vessel, port, distance, depth, systems, **kwargs): + """ + Logic for the Mooring System Installation Vessel. + + Parameters + ---------- + vessel : Vessel + Mooring System Installation Vessel + port : Port + distance : int | float + Distance between port and site (km). + systems : int + Total systems to install. + """ + + n = 0 + while n < systems: + if vessel.at_port: + try: + # Get mooring systems from port. + yield get_list_of_items_from_port( + vessel, port, ["MooringSystem"], **kwargs + ) + + except ItemNotFound: + # If no items are at port and vessel.storage.items is empty, + # the job is done + if not vessel.storage.items: + vessel.submit_debug_log( + message="Item not found. Shutting down." + ) + break + + # Transit to site + vessel.update_trip_data() + vessel.at_port = False + yield vessel.transit(distance) + vessel.at_site = True + + if vessel.at_site: + + if vessel.storage.items: + + system = yield vessel.get_item_from_storage( + "MooringSystem", **kwargs + ) + for _ in range(system.num_lines): + yield position_onsite(vessel, **kwargs) + yield perform_mooring_site_survey(vessel, **kwargs) + yield install_mooring_anchor( + vessel, depth, system.anchor_type, **kwargs + ) + yield install_mooring_line(vessel, depth, **kwargs) + + n += 1 + + else: + # Transit to port + vessel.at_site = False + yield vessel.transit(distance) + vessel.at_port = True + + vessel.submit_debug_log(message="Mooring systems installation complete!") + + +@process +def perform_mooring_site_survey(vessel, **kwargs): + """ + Calculates time required to perform a mooring system survey. + + Parameters + ---------- + vessel : Vessel + Vessel to perform action. + + Yields + ------ + vessel.task representing time to "Perform Mooring Site Survey". + """ + + key = "mooring_site_survey_time" + survey_time = kwargs.get(key, pt[key]) + + yield vessel.task( + "Perform Mooring Site Survey", + survey_time, + constraints=vessel.transit_limits, + **kwargs, + ) + + +@process +def install_mooring_anchor(vessel, depth, _type, **kwargs): + """ + Calculates time required to install a mooring system anchor. + + Parameters + ---------- + vessel : Vessel + Vessel to perform action. + depth : int | float + Depth at site (m). + _type : str + Anchor type. 'Suction Pile' or 'Drag Embedment'. + + Yields + ------ + vessel.task representing time to install mooring anchor. + """ + + if _type == "Suction Pile": + key = "suction_pile_install_time" + task = "Install Suction Pile Anchor" + fixed = kwargs.get(key, pt[key]) + + elif _type == "Drag Embedment": + key = "drag_embed_install_time" + task = "Install Drag Embedment Anchor" + fixed = kwargs.get(key, pt[key]) + + else: + raise ValueError( + f"Mooring System Anchor Type: {_type} not recognized." + ) + + install_time = fixed + 0.005 * depth + yield vessel.task( + task, install_time, constraints=vessel.transit_limits, **kwargs + ) + + +@process +def install_mooring_line(vessel, depth, **kwargs): + """ + Calculates time required to install a mooring system line. + + Parameters + ---------- + vessel : Vessel + Vessel to perform action. + depth : int | float + Depth at site (m). + + Yields + ------ + vessel.task representing time to install mooring line. + """ + + install_time = 0.005 * depth + + yield vessel.task( + "Install Mooring Line", + install_time, + constraints=vessel.transit_limits, + **kwargs, + ) + + +class MooringSystem(Cargo): + """Mooring System Cargo""" + + def __init__( + self, + num_lines=None, + line_mass=None, + anchor_mass=None, + anchor_type="Suction Pile", + **kwargs, + ): + """Creates an instance of MooringSystem""" + + self.num_lines = num_lines + self.line_mass = line_mass + self.anchor_mass = anchor_mass + self.anchor_type = anchor_type + + self.deck_space = 0 + + @property + def mass(self): + """Returns total system mass in t.""" + + return self.num_lines * (self.line_mass + self.anchor_mass) + + @staticmethod + def fasten(**kwargs): + """Dummy method to work with `get_list_of_items_from_port`.""" + + key = "mooring_system_load_time" + time = kwargs.get(key, pt[key]) + + return "Load Mooring System", time + + @staticmethod + def release(**kwargs): + """Dummy method to work with `get_list_of_items_from_port`.""" + + return "", 0 + + def anchor_install_time(self, depth): + """ + Returns time to install anchor. Varies by depth. + + Parameters + ---------- + depth : int | float + Depth at site (m). + """ + + if self.anchor_type == "Suction Pile": + fixed = 11 + + elif self.anchor_type == "Drag Embedment": + fixed = 5 + + else: + raise ValueError( + f"Mooring System Anchor Type: {self.anchor_type} not recognized." + ) + + return fixed + 0.005 * depth diff --git a/ORBIT/phases/install/oss_install/standard.py b/ORBIT/phases/install/oss_install/standard.py index 6837d2e3..07907229 100644 --- a/ORBIT/phases/install/oss_install/standard.py +++ b/ORBIT/phases/install/oss_install/standard.py @@ -37,7 +37,7 @@ class OffshoreSubstationInstallation(InstallPhase): "feeder": "dict | str", "site": {"distance": "km", "depth": "m"}, "port": { - "num_cranes": "int", + "num_cranes": "int (optional, default: 1)", "monthly_rate": "USD/mo (optional)", "name": "str (optional)", }, diff --git a/ORBIT/phases/install/quayside_assembly_tow/__init__.py b/ORBIT/phases/install/quayside_assembly_tow/__init__.py new file mode 100644 index 00000000..e97eed02 --- /dev/null +++ b/ORBIT/phases/install/quayside_assembly_tow/__init__.py @@ -0,0 +1,10 @@ +"""Quayside assembly and tow-out modules.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from .moored import MooredSubInstallation +from .gravity_base import GravityBasedInstallation diff --git a/ORBIT/phases/install/quayside_assembly_tow/common.py b/ORBIT/phases/install/quayside_assembly_tow/common.py new file mode 100644 index 00000000..252965b7 --- /dev/null +++ b/ORBIT/phases/install/quayside_assembly_tow/common.py @@ -0,0 +1,391 @@ +"""Common processes and cargo types for quayside assembly and tow-out +installations""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from marmot import Agent, le, process +from marmot._exceptions import AgentNotRegistered + + +class Substructure: + """Floating Substructure Class.""" + + def __init__(self): + """Creates an instance of `Substructure`.""" + + pass + + +class SubstructureAssemblyLine(Agent): + """Substructure Assembly Line Class.""" + + def __init__(self, assigned, time, target, num): + """ + Creates an instance of `SubstructureAssemblyLine`. + + Parameters + ---------- + assigned : list + List of assigned tasks. Can be shared with other assembly lines. + time : int | float + Hours required to produce one substructure. + target : simpy.Store + Target storage. + num : int + Assembly line number designation. + """ + + super().__init__(f"Substructure Assembly Line {num}") + + self.assigned = assigned + self.time = time + self.target = target + + def submit_action_log(self, action, duration, **kwargs): + """ + Submits a log representing a completed `action` performed over time + `duration`. + + This method overwrites the default `submit_action_log` in + `marmot.Agent`, adding operation cost to every submitted log within + ORBIT. + + Parameters + ---------- + action : str + Performed action. + duration : int | float + Duration of action. + + Raises + ------ + AgentNotRegistered + """ + + if self.env is None: + raise AgentNotRegistered(self) + + else: + payload = { + **kwargs, + "agent": str(self), + "action": action, + "duration": float(duration), + "cost": 0, + } + + self.env._submit_log(payload, level="ACTION") + + @process + def assemble_substructure(self): + """ + Simulation process for assembling a substructure. + """ + + yield self.task("Substructure Assembly", self.time) + substructure = Substructure() + + start = self.env.now + yield self.target.put(substructure) + delay = self.env.now - start + + if delay > 0: + self.submit_action_log("Delay: No Wet Storage Available", delay) + + @process + def start(self): + """ + Trigger the assembly line to run. Will attempt to pull a task from + self.assigned and timeout for the assembly time. Shuts down after + self.assigned is empty. + """ + + while True: + try: + _ = self.assigned.pop(0) + yield self.assemble_substructure() + + except IndexError: + break + + +class TurbineAssemblyLine(Agent): + """Turbine Assembly Line Class.""" + + def __init__(self, feed, target, turbine, num): + """ + Creates an instance of `TurbineAssemblyLine`. + + Parameters + ---------- + feed : simpy.Store + Storage for completed substructures. + target : simpy.Store + Target storage. + num : int + Assembly line number designation. + """ + + super().__init__(f"Turbine Assembly Line {num}") + + self.feed = feed + self.target = target + self.turbine = turbine + + def submit_action_log(self, action, duration, **kwargs): + """ + Submits a log representing a completed `action` performed over time + `duration`. + + This method overwrites the default `submit_action_log` in + `marmot.Agent`, adding operation cost to every submitted log within + ORBIT. + + Parameters + ---------- + action : str + Performed action. + duration : int | float + Duration of action. + + Raises + ------ + AgentNotRegistered + """ + + if self.env is None: + raise AgentNotRegistered(self) + + else: + payload = { + **kwargs, + "agent": str(self), + "action": action, + "duration": float(duration), + "cost": 0, + } + + self.env._submit_log(payload, level="ACTION") + + @process + def start(self): + """ + Trigger the assembly line to run. Will attempt to pull a task from + self.assigned and timeout for the assembly time. Shuts down after + self.assigned is empty. + """ + + while True: + start = self.env.now + sub = yield self.feed.get() + delay = self.env.now - start + + if delay > 0: + self.submit_action_log( + "Delay: No Substructures in Wet Storage", delay + ) + + yield self.assemble_turbine() + + @process + def assemble_turbine(self): + """ + Turbine assembly process. Follows a similar process as the + `TurbineInstallation` modules but has fixed lift times + fasten times + instead of calculating the lift times dynamically. + """ + + yield self.move_substructure() + yield self.prepare_for_assembly() + + sections = self.turbine["tower"].get("sections", 1) + for _ in range(sections): + yield self.lift_and_attach_tower_section() + + yield self.lift_and_attach_nacelle() + + for _ in range(3): + yield self.lift_and_attach_blade() + + yield self.mechanical_completion() + + start = self.env.now + yield self.target.put(1) + delay = self.env.now - start + + if delay > 0: + self.submit_action_log( + "Delay: No Assembly Storage Available", delay + ) + + self.submit_debug_log( + message="Assembly delievered to installation groups." + ) + + @process + def move_substructure(self): + """ + Task representing time associated with moving the completed + substructure assembly to the turbine assembly line. + + TODO: Move to dynamic process involving tow groups. + """ + + yield self.task("Move Substructure", 8) + + @process + def prepare_for_assembly(self): + """ + Task representing time associated with preparing a substructure for + turbine assembly. + """ + + yield self.task("Prepare for Turbine Assembly", 12) + + @process + def lift_and_attach_tower_section(self): + """ + Task representing time associated with lifting and attaching a tower + section at quayside. + """ + + yield self.task( + "Lift and Attach Tower Section", + 12, + constraints={"windspeed": le(15)}, + ) + + @process + def lift_and_attach_nacelle(self): + """ + Task representing time associated with lifting and attaching a nacelle + at quayside. + """ + + yield self.task( + "Lift and Attach Nacelle", 7, constraints={"windspeed": le(15)} + ) + + @process + def lift_and_attach_blade(self): + """ + Task representing time associated with lifting and attaching a turbine + blade at quayside. + """ + + yield self.task( + "Lift and Attach Blade", 3.5, constraints={"windspeed": le(12)} + ) + + @process + def mechanical_completion(self): + """ + Task representing time associated with performing mechanical compltion + work at quayside. + """ + + yield self.task( + "Mechanical Completion", 24, constraints={"windspeed": le(18)} + ) + + +class TowingGroup(Agent): + """Class to represent an arbitrary group of towing vessels.""" + + def __init__(self, vessel_specs, num=1): + """ + Creates an instance of TowingGroup. + + Parameters + ---------- + vessel_specs : dict + Specs for the individual vessels used in the towing group. + Currently restricted to one vessel specification per group. + """ + + super().__init__(f"Towing Group {num}") + self._specs = vessel_specs + self.day_rate = self._specs["vessel_specs"]["day_rate"] + self.transit_speed = self._specs["transport_specs"]["transit_speed"] + + def initialize(self): + """Initializes the towing group.""" + + self.submit_debug_log(message="{self.name} initialized.") + + @process + def group_task( + self, name, duration, num_vessels, constraints={}, **kwargs + ): + """ + Submits a group task with any number of towing vessels. + + Parameters + ---------- + name : str + Name of task to complete. Used for submitting action logs. + duration : float | int + Duration of the task. + Rounded up to the nearest int. + num_vessels : int + Number of individual towing vessels needed for the operation. + """ + + kwargs = {**kwargs, "num_vessels": num_vessels} + yield self.task(name, duration, constraints=constraints, **kwargs) + + def operation_cost(self, hours, **kwargs): + """ + Returns cost of an operation of duration `hours` using number of + vessels, `num_vessels`. + + Parameters + ---------- + hours : int | float + Duration of operation in hours. + vessels : int + Default: 1 + """ + + mult = kwargs.get("cost_multiplier", 1.0) + vessels = kwargs.get("num_vessels", 1) + return (self.day_rate / 24) * vessels * hours * mult + + def submit_action_log(self, action, duration, **kwargs): + """ + Submits a log representing a completed `action` performed over time + `duration`. + + This method overwrites the default `submit_action_log` in + `marmot.Agent`, adding operation cost to every submitted log within + ORBIT. + + Parameters + ---------- + action : str + Performed action. + duration : int | float + Duration of action. + + Raises + ------ + AgentNotRegistered + """ + + if self.env is None: + raise AgentNotRegistered(self) + + else: + payload = { + **kwargs, + "agent": str(self), + "action": action, + "duration": float(duration), + "cost": self.operation_cost(duration, **kwargs), + } + + self.env._submit_log(payload, level="ACTION") diff --git a/ORBIT/phases/install/quayside_assembly_tow/gravity_base.py b/ORBIT/phases/install/quayside_assembly_tow/gravity_base.py new file mode 100644 index 00000000..c2398a81 --- /dev/null +++ b/ORBIT/phases/install/quayside_assembly_tow/gravity_base.py @@ -0,0 +1,403 @@ +"""Installation strategies for gravity-base substructures.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +import simpy +from marmot import le, process + +from ORBIT.core import Vessel, WetStorage +from ORBIT.phases.install import InstallPhase + +from .common import TowingGroup, TurbineAssemblyLine, SubstructureAssemblyLine + + +class GravityBasedInstallation(InstallPhase): + """ + Installation module to model the quayside assembly, tow-out and + installation of gravity based foundations. + """ + + phase = "Gravity Based Foundation Installation" + + #: + expected_config = { + "support_vessel": "str", + "towing_vessel": "str", + "towing_vessel_groups": { + "towing_vessels": "int", + "station_keeping_vessels": "int", + "num_groups": "int (optional)", + }, + "substructure": { + "takt_time": "int | float (optional, default: 0)", + "towing_speed": "int | float (optional, default: 6 km/h)", + }, + "site": {"depth": "m", "distance": "km"}, + "plant": {"num_turbines": "int"}, + "turbine": "dict", + "port": { + "sub_assembly_lines": "int (optional, default: 1)", + "sub_storage": "int (optional, default: inf)", + "turbine_assembly_cranes": "int (optional, default: 1)", + "assembly_storage": "int (optional, default: inf)", + "monthly_rate": "USD/mo (optional)", + "name": "str (optional)", + }, + } + + def __init__(self, config, weather=None, **kwargs): + """ + Creates an instance of GravityBasedInstallation. + + Parameters + ---------- + config : dict + Simulation specific configuration. + weather : np.array + Weather data at site. + """ + + super().__init__(weather, **kwargs) + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.extract_defaults() + + self.setup_simulation(**kwargs) + + def setup_simulation(self, **kwargs): + """ + Sets up simulation infrastructure. + - Initializes substructure production + - Initializes turbine assembly processes + - Initializes towing groups + """ + + self.distance = self.config["site"]["distance"] + self.num_turbines = self.config["plant"]["num_turbines"] + + self.initialize_substructure_production() + self.initialize_turbine_assembly() + self.initialize_queue() + self.initialize_towing_groups() + self.initialize_support_vessel() + + def initialize_substructure_production(self): + """ + Initializes the production of substructures at port. The number of + independent assembly lines and production time associated with a + substructure can be configured with the following parameters: + + - self.config["substructure"]["takt_time"] + - self.config["port"]["sub_assembly_lines"] + """ + + try: + storage = self.config["port"]["sub_storage"] + + except KeyError: + storage = float("inf") + + self.wet_storage = WetStorage(self.env, storage) + + try: + time = self.config["substructure"]["takt_time"] + + except KeyError: + time = 0 + + try: + lines = self.config["port"]["sub_assembly_lines"] + + except KeyError: + lines = 1 + + num = self.config["plant"]["num_turbines"] + to_assemble = [1] * num + + self.sub_assembly_lines = [] + for i in range(lines): + a = SubstructureAssemblyLine( + to_assemble, time, self.wet_storage, i + 1 + ) + + self.env.register(a) + a.start() + self.sub_assembly_lines.append(a) + + def initialize_turbine_assembly(self): + """ + Initializes turbine assembly lines. The number of independent lines + can be configured with the following parameters: + + - self.config["port"]["turb_assembly_lines"] + """ + + try: + storage = self.config["port"]["assembly_storage"] + + except KeyError: + storage = float("inf") + + self.assembly_storage = WetStorage(self.env, storage) + + try: + lines = self.config["port"]["turbine_assembly_cranes"] + + except KeyError: + lines = 1 + + turbine = self.config["turbine"] + self.turbine_assembly_lines = [] + for i in range(lines): + a = TurbineAssemblyLine( + self.wet_storage, self.assembly_storage, turbine, i + 1 + ) + + self.env.register(a) + a.start() + self.turbine_assembly_lines.append(a) + + def initialize_towing_groups(self, **kwargs): + """ + Initializes towing groups to bring completed assemblies to site and + stabilize the assembly during final installation. + """ + + self.installation_groups = [] + + vessel = self.config["towing_vessel"] + num_groups = self.config["towing_vessel_groups"].get("num_groups", 1) + towing = self.config["towing_vessel_groups"]["towing_vessels"] + towing_speed = self.config["substructure"].get("towing_speed", 6) + + for i in range(num_groups): + g = TowingGroup(vessel, num=i + 1) + self.env.register(g) + g.initialize() + self.installation_groups.append(g) + + transfer_gbf_substructures_from_storage( + g, + self.assembly_storage, + self.distance, + self.active_group, + towing, + towing_speed, + **kwargs, + ) + + def initialize_queue(self): + """ + Initializes the queue, modeled as a ``SimPy.Resource`` that towing + groups join at site. + """ + + self.active_group = simpy.Resource(self.env, capacity=1) + self.active_group.vessel = None + self.active_group.activate = self.env.event() + + def initialize_support_vessel(self, **kwargs): + """ + Initializes Multi-Purpose Support Vessel to perform installation + processes at site. + """ + + specs = self.config["support_vessel"] + vessel = Vessel("Multi-Purpose Support Vessel", specs) + + self.env.register(vessel) + vessel.initialize(mobilize=False) + self.support_vessel = vessel + + station_keeping_vessels = self.config["towing_vessel_groups"][ + "station_keeping_vessels" + ] + + install_gravity_base_foundations( + self.support_vessel, + self.active_group, + self.distance, + self.num_turbines, + station_keeping_vessels, + **kwargs, + ) + + @property + def detailed_output(self): + """""" + + return { + "operational_delays": { + **{ + k: self.operational_delay(str(k)) + for k in self.sub_assembly_lines + }, + **{ + k: self.operational_delay(str(k)) + for k in self.turbine_assembly_lines + }, + **{ + k: self.operational_delay(str(k)) + for k in self.installation_groups + }, + self.support_vessel: self.operational_delay( + str(self.support_vessel) + ), + } + } + + def operational_delay(self, name): + """""" + + actions = [a for a in self.env.actions if a["agent"] == name] + delay = sum(a["duration"] for a in actions if "Delay" in a["action"]) + + return delay + + +@process +def transfer_gbf_substructures_from_storage( + group, feed, distance, queue, towing_vessels, towing_speed, **kwargs +): + """ + Process logic for the towing vessel group. + + Parameters + ---------- + group : Vessel + Towing group. + feed : simpy.Store + Completed assembly storage. + distance : int | float + Distance from port to site. + towing_vessels : int + Number of vessels to use for towing to site. + towing_speed : int | float + Configured towing speed (km/h) + """ + + towing_time = distance / towing_speed + transit_time = distance / group.transit_speed + + while True: + + start = group.env.now + assembly = yield feed.get() + delay = group.env.now - start + + if delay > 0: + group.submit_action_log( + "Delay: No Completed Assemblies Available", delay + ) + + yield group.group_task( + "Tow Substructure", towing_time, num_vessels=towing_vessels + ) + + # At Site + with queue.request() as req: + queue_start = group.env.now + yield req + + queue_time = group.env.now - queue_start + if queue_time > 0: + group.submit_action_log("Queue", queue_time, location="Site") + + queue.vessel = group + active_start = group.env.now + queue.activate.succeed() + + # Released by WTIV when objects are depleted + group.release = group.env.event() + yield group.release + active_time = group.env.now - active_start + + queue.vessel = None + queue.activate = group.env.event() + + yield group.group_task( + "Transit", transit_time, num_vessels=towing_vessels + ) + + +@process +def install_gravity_base_foundations( + vessel, queue, distance, substructures, station_keeping_vessels, **kwargs +): + """ + Logic that a Multi-Purpose Support Vessel uses at site to complete the + installation of gravity based foundations. + + Parameters + ---------- + vessel : Vessel + queue : + distance : int | float + Distance between port and site (km). + substructures : int + Number of substructures to install before transiting back to port. + station_keeping_vessels : int + Number of vessels to use for substructure station keeping during final + installation at site. + """ + + n = 0 + while n < substructures: + if queue.vessel: + + start = vessel.env.now + if n == 0: + vessel.mobilize() + yield vessel.transit(distance) + + yield vessel.task( + "Position Substructure", + 5, + constraints={"windspeed": le(15), "waveheight": le(2)}, + ) + yield vessel.task( + "ROV Survey", + 1, + constraints={"windspeed": le(25), "waveheight": le(3)}, + ) + + # TODO: Model for ballast pump time + yield vessel.task( + "Pump Ballast", + 12, + # suspendable=True, + constraints={"windspeed": le(15), "waveheight": le(2)}, + ) + + # TODO: Model for GBF grout time + yield vessel.task( + "Grout GBF", + 6, + suspendable=True, + constraints={"windspeed": le(15), "waveheight": le(2)}, + ) + + group_time = vessel.env.now - start + queue.vessel.submit_action_log( + "Positioning Support", + group_time, + location="site", + num_vessels=station_keeping_vessels, + ) + yield queue.vessel.release.succeed() + n += 1 + + else: + start = vessel.env.now + yield queue.activate + delay_time = vessel.env.now - start + + if n != 0: + vessel.submit_action_log("Delay", delay_time, location="Site") + + yield vessel.transit(distance) diff --git a/ORBIT/phases/install/quayside_assembly_tow/moored.py b/ORBIT/phases/install/quayside_assembly_tow/moored.py new file mode 100644 index 00000000..94260787 --- /dev/null +++ b/ORBIT/phases/install/quayside_assembly_tow/moored.py @@ -0,0 +1,408 @@ +"""Installation strategies for moored floating systems.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +import simpy +from marmot import le, process + +from ORBIT.core import Vessel, WetStorage +from ORBIT.phases.install import InstallPhase + +from .common import TowingGroup, TurbineAssemblyLine, SubstructureAssemblyLine + + +class MooredSubInstallation(InstallPhase): + """ + Installation module to model the quayside assembly, tow-out and + installation at sea of moored substructures. + """ + + phase = "Moored Substructure Installation" + + #: + expected_config = { + "support_vessel": "str", + "towing_vessel": "str", + "towing_vessel_groups": { + "towing_vessels": "int", + "station_keeping_vessels": "int", + "num_groups": "int (optional)", + }, + "substructure": { + "takt_time": "int | float (optional, default: 0)", + "towing_speed": "int | float (optional, default: 6 km/h)", + }, + "site": {"depth": "m", "distance": "km"}, + "plant": {"num_turbines": "int"}, + "turbine": "dict", + "port": { + "sub_assembly_lines": "int (optional, default: 1)", + "sub_storage": "int (optional, default: inf)", + "turbine_assembly_cranes": "int (optional, default: 1)", + "assembly_storage": "int (optional, default: inf)", + "monthly_rate": "USD/mo (optional)", + "name": "str (optional)", + }, + } + + def __init__(self, config, weather=None, **kwargs): + """ + Creates an instance of MooredSubInstallation. + + Parameters + ---------- + config : dict + Simulation specific configuration. + weather : np.array + Weather data at site. + """ + + super().__init__(weather, **kwargs) + + config = self.initialize_library(config, **kwargs) + self.config = self.validate_config(config) + self.extract_defaults() + + self.setup_simulation(**kwargs) + + def setup_simulation(self, **kwargs): + """ + Sets up simulation infrastructure. + - Initializes substructure production + - Initializes turbine assembly processes + - Initializes towing groups + """ + + self.distance = self.config["site"]["distance"] + self.num_turbines = self.config["plant"]["num_turbines"] + + self.initialize_substructure_production() + self.initialize_turbine_assembly() + self.initialize_queue() + self.initialize_towing_groups() + self.initialize_support_vessel() + + def initialize_substructure_production(self): + """ + Initializes the production of substructures at port. The number of + independent assembly lines and production time associated with a + substructure can be configured with the following parameters: + + - self.config["substructure"]["takt_time"] + - self.config["port"]["sub_assembly_lines"] + """ + + try: + storage = self.config["port"]["sub_storage"] + + except KeyError: + storage = float("inf") + + self.wet_storage = WetStorage(self.env, storage) + + try: + time = self.config["substructure"]["takt_time"] + + except KeyError: + time = 0 + + try: + lines = self.config["port"]["sub_assembly_lines"] + + except KeyError: + lines = 1 + + to_assemble = [1] * self.num_turbines + + self.sub_assembly_lines = [] + for i in range(lines): + a = SubstructureAssemblyLine( + to_assemble, time, self.wet_storage, i + 1 + ) + + self.env.register(a) + a.start() + self.sub_assembly_lines.append(a) + + def initialize_turbine_assembly(self): + """ + Initializes turbine assembly lines. The number of independent lines + can be configured with the following parameters: + + - self.config["port"]["turb_assembly_lines"] + """ + + try: + storage = self.config["port"]["assembly_storage"] + + except KeyError: + storage = float("inf") + + self.assembly_storage = WetStorage(self.env, storage) + + try: + lines = self.config["port"]["turbine_assembly_cranes"] + + except KeyError: + lines = 1 + + turbine = self.config["turbine"] + self.turbine_assembly_lines = [] + for i in range(lines): + a = TurbineAssemblyLine( + self.wet_storage, self.assembly_storage, turbine, i + 1 + ) + + self.env.register(a) + a.start() + self.turbine_assembly_lines.append(a) + + def initialize_towing_groups(self, **kwargs): + """ + Initializes towing groups to bring completed assemblies to site and + stabilize the assembly during final installation. + """ + + self.installation_groups = [] + + vessel = self.config["towing_vessel"] + num_groups = self.config["towing_vessel_groups"].get("num_groups", 1) + towing = self.config["towing_vessel_groups"]["towing_vessels"] + towing_speed = self.config["substructure"].get("towing_speed", 6) + + for i in range(num_groups): + g = TowingGroup(vessel, num=i + 1) + self.env.register(g) + g.initialize() + self.installation_groups.append(g) + + transfer_moored_substructures_from_storage( + g, + self.assembly_storage, + self.distance, + self.active_group, + towing, + towing_speed, + **kwargs, + ) + + def initialize_queue(self): + """ + Initializes the queue, modeled as a ``SimPy.Resource`` that towing + groups join at site. + """ + + self.active_group = simpy.Resource(self.env, capacity=1) + self.active_group.vessel = None + self.active_group.activate = self.env.event() + + def initialize_support_vessel(self, **kwargs): + """ + Initializes Multi-Purpose Support Vessel to perform installation + processes at site. + """ + + specs = self.config["support_vessel"] + vessel = Vessel("Multi-Purpose Support Vessel", specs) + + self.env.register(vessel) + vessel.initialize(mobilize=False) + self.support_vessel = vessel + + station_keeping_vessels = self.config["towing_vessel_groups"][ + "station_keeping_vessels" + ] + + install_moored_substructures( + self.support_vessel, + self.active_group, + self.distance, + self.num_turbines, + station_keeping_vessels, + **kwargs, + ) + + @property + def detailed_output(self): + """""" + + return { + "operational_delays": { + **{ + k: self.operational_delay(str(k)) + for k in self.sub_assembly_lines + }, + **{ + k: self.operational_delay(str(k)) + for k in self.turbine_assembly_lines + }, + **{ + k: self.operational_delay(str(k)) + for k in self.installation_groups + }, + self.support_vessel: self.operational_delay( + str(self.support_vessel) + ), + } + } + + def operational_delay(self, name): + """""" + + actions = [a for a in self.env.actions if a["agent"] == name] + delay = sum(a["duration"] for a in actions if "Delay" in a["action"]) + + return delay + + +@process +def transfer_moored_substructures_from_storage( + group, feed, distance, queue, towing_vessels, towing_speed, **kwargs +): + """ + Process logic for the towing vessel group. + + Parameters + ---------- + group : Vessel + Towing group. + feed : simpy.Store + Completed assembly storage. + distance : int | float + Distance from port to site. + towing_vessels : int + Number of vessels to use for towing to site. + towing_speed : int | float + Configured towing speed (km/h) + """ + + towing_time = distance / towing_speed + transit_time = distance / group.transit_speed + + while True: + + start = group.env.now + assembly = yield feed.get() + delay = group.env.now - start + + if delay > 0: + group.submit_action_log( + "Delay: No Completed Assemblies Available", delay + ) + + yield group.group_task( + "Ballast to Towing Draft", + 6, + num_vessels=towing_vessels, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + + yield group.group_task( + "Tow Substructure", + towing_time, + num_vessels=towing_vessels, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + + # At Site + with queue.request() as req: + queue_start = group.env.now + yield req + + queue_time = group.env.now - queue_start + if queue_time > 0: + group.submit_action_log("Queue", queue_time, location="Site") + + queue.vessel = group + active_start = group.env.now + queue.activate.succeed() + + # Released by WTIV when objects are depleted + group.release = group.env.event() + yield group.release + active_time = group.env.now - active_start + + queue.vessel = None + queue.activate = group.env.event() + + yield group.group_task( + "Transit", transit_time, num_vessels=towing_vessels + ) + + +@process +def install_moored_substructures( + vessel, queue, distance, substructures, station_keeping_vessels, **kwargs +): + """ + Logic that a Multi-Purpose Support Vessel uses at site to complete the + installation of moored substructures. + + Parameters + ---------- + vessel : Vessel + queue : + distance : int | float + Distance between port and site (km). + substructures : int + Number of substructures to install before transiting back to port. + station_keeping_vessels : int + Number of vessels to use for substructure station keeping during final + installation at site. + """ + + n = 0 + while n < substructures: + if queue.vessel: + + start = vessel.env.now + if n == 0: + vessel.mobilize() + yield vessel.transit(distance) + + yield vessel.task( + "Position Substructure", + 2, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + yield vessel.task( + "Ballast to Operational Draft", + 6, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + yield vessel.task( + "Connect Mooring Lines", + 22, + suspendable=True, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + yield vessel.task( + "Check Mooring Lines", + 12, + suspendable=True, + constraints={"windspeed": le(15), "waveheight": le(2.5)}, + ) + + group_time = vessel.env.now - start + queue.vessel.submit_action_log( + "Positioning Support", + group_time, + location="site", + num_vessels=station_keeping_vessels, + ) + yield queue.vessel.release.succeed() + n += 1 + + else: + start = vessel.env.now + yield queue.activate + delay_time = vessel.env.now - start + + if n != 0: + vessel.submit_action_log("Delay", delay_time, location="Site") + + yield vessel.transit(distance) diff --git a/ORBIT/phases/install/turbine_install/standard.py b/ORBIT/phases/install/turbine_install/standard.py index 8b1e681f..d4683cbb 100644 --- a/ORBIT/phases/install/turbine_install/standard.py +++ b/ORBIT/phases/install/turbine_install/standard.py @@ -51,7 +51,7 @@ class TurbineInstallation(InstallPhase): "site": {"depth": "m", "distance": "km"}, "plant": {"num_turbines": "int"}, "port": { - "num_cranes": "int", + "num_cranes": "int (optional, default: 1)", "monthly_rate": "USD/mo (optional)", "name": "str (optional)", }, diff --git a/docs/source/api_DesignPhase.rst b/docs/source/api_DesignPhase.rst index 5dedfd1c..b668a8a1 100644 --- a/docs/source/api_DesignPhase.rst +++ b/docs/source/api_DesignPhase.rst @@ -16,5 +16,8 @@ trends but are not intended to be used for actual designs. phases/design/api_ArraySystemDesign phases/design/api_ExportSystemDesign phases/design/api_OffshoreSubstationDesign + phases/design/api_SemiSubmersibleDesign + phases/design/api_SparDesign + phases/design/api_MooringSystemDesign .. phases/design/api_JacketDesign diff --git a/docs/source/api_InstallPhase.rst b/docs/source/api_InstallPhase.rst index 77ef2189..8902c390 100644 --- a/docs/source/api_InstallPhase.rst +++ b/docs/source/api_InstallPhase.rst @@ -17,5 +17,7 @@ description of vessel scheduling within ORBIT, please see `add link`. phases/install/array/api_ArrayCableInstall phases/install/export/api_ExportCableInstall phases/install/oss/api_OffshoreSubstationInstall + phases/install/quayside_towout/api_MooredSubInstallation + phases/install/mooring/api_MooringSystemInstallation .. phases/install/jacket/api_JacketInstall diff --git a/docs/source/doc_DesignPhase.rst b/docs/source/doc_DesignPhase.rst index 3a756701..f4121504 100644 --- a/docs/source/doc_DesignPhase.rst +++ b/docs/source/doc_DesignPhase.rst @@ -14,5 +14,8 @@ the model. phases/design/doc_ArraySystemDesign phases/design/doc_ExportSystemDesign phases/design/doc_OffshoreSubstationDesign + phases/design/doc_SemiSubmersibleDesign + phases/design/doc_SparDesign + phases/design/doc_MooringSystemDesign .. phases/design/doc_JacketDesign diff --git a/docs/source/doc_InstallPhase.rst b/docs/source/doc_InstallPhase.rst index f8a13456..be568c3e 100644 --- a/docs/source/doc_InstallPhase.rst +++ b/docs/source/doc_InstallPhase.rst @@ -15,5 +15,7 @@ available in the model. phases/install/array/doc_ArrayCableInstall phases/install/export/doc_ExportCableInstall phases/install/oss/doc_OffshoreSubstationInstall + phases/install/quayside_towout/doc_MooredSubInstallation + phases/install/mooring/doc_MooringSystemInstallation .. phases/install/jacket/doc_JacketInstall diff --git a/docs/source/phases/design/api_MooringSystemDesign.rst b/docs/source/phases/design/api_MooringSystemDesign.rst new file mode 100644 index 00000000..bdc7b7a1 --- /dev/null +++ b/docs/source/phases/design/api_MooringSystemDesign.rst @@ -0,0 +1,8 @@ +Mooring System Design API +========================= + +For detailed methodology, please see +:doc:`Mooring System Design `. + +.. autoclass:: ORBIT.phases.design.MooringSystemDesign + :members: diff --git a/docs/source/phases/design/api_SemiSubmersibleDesign.rst b/docs/source/phases/design/api_SemiSubmersibleDesign.rst new file mode 100644 index 00000000..ed3ba2b6 --- /dev/null +++ b/docs/source/phases/design/api_SemiSubmersibleDesign.rst @@ -0,0 +1,8 @@ +Semi-Submersible Design API +=========================== + +For detailed methodology, please see +:doc:`Semi-Submersible Design `. + +.. autoclass:: ORBIT.phases.design.SemiSubmersibleDesign + :members: diff --git a/docs/source/phases/design/api_SparDesign.rst b/docs/source/phases/design/api_SparDesign.rst new file mode 100644 index 00000000..21e1593d --- /dev/null +++ b/docs/source/phases/design/api_SparDesign.rst @@ -0,0 +1,8 @@ +Spar Design API +=============== + +For detailed methodology, please see +:doc:`Spar Design `. + +.. autoclass:: ORBIT.phases.design.SparDesign + :members: diff --git a/docs/source/phases/design/doc_MooringSystemDesign.rst b/docs/source/phases/design/doc_MooringSystemDesign.rst new file mode 100644 index 00000000..cc6a5df2 --- /dev/null +++ b/docs/source/phases/design/doc_MooringSystemDesign.rst @@ -0,0 +1,17 @@ +Mooring System Design Methodology +================================= + +For details of the code implementation, please see +:doc:`Mooring System Design API `. + +Overview +-------- + +The mooring system design module in ORBIT is based on previous modeling +efforts undertaken by NREL, [#maness2017]_. + +References +---------- + +.. [#maness2017] Michael Maness, Benjamin Maples, Aaron Smith, + NREL Offshore Balance-of-System Model, 2017 diff --git a/docs/source/phases/design/doc_SemiSubmersibleDesign.rst b/docs/source/phases/design/doc_SemiSubmersibleDesign.rst new file mode 100644 index 00000000..5e181fdf --- /dev/null +++ b/docs/source/phases/design/doc_SemiSubmersibleDesign.rst @@ -0,0 +1,17 @@ +Semi-Submersible Design Methodology +=================================== + +For details of the code implementation, please see +:doc:`Semi-Submersible Design API `. + +Overview +-------- + +The semi-submersible design module in ORBIT is based on previous modeling +efforts undertaken by NREL, [#maness2017]_. + +References +---------- + +.. [#maness2017] Michael Maness, Benjamin Maples, Aaron Smith, + NREL Offshore Balance-of-System Model, 2017 diff --git a/docs/source/phases/design/doc_SparDesign.rst b/docs/source/phases/design/doc_SparDesign.rst new file mode 100644 index 00000000..6200b85f --- /dev/null +++ b/docs/source/phases/design/doc_SparDesign.rst @@ -0,0 +1,17 @@ +Spar Design Methodology +======================= + +For details of the code implementation, please see +:doc:`Spar Design API `. + +Overview +-------- + +The spar design module in ORBIT is based on previous modeling efforts +undertaken by NREL, [#maness2017]_. + +References +---------- + +.. [#maness2017] Michael Maness, Benjamin Maples, Aaron Smith, + NREL Offshore Balance-of-System Model, 2017 diff --git a/docs/source/phases/install/mooring/api_MooringSystemInstallation.rst b/docs/source/phases/install/mooring/api_MooringSystemInstallation.rst new file mode 100644 index 00000000..2f2295a5 --- /dev/null +++ b/docs/source/phases/install/mooring/api_MooringSystemInstallation.rst @@ -0,0 +1,8 @@ +Mooring System Installation API +=============================== + +For detailed methodology, please see +:doc:`Mooring System Installation Methodology `. + +.. autoclass:: ORBIT.phases.install.MooringSystemInstallation + :members: diff --git a/docs/source/phases/install/mooring/doc_MooringSystemInstallation.rst b/docs/source/phases/install/mooring/doc_MooringSystemInstallation.rst new file mode 100644 index 00000000..bd04b81d --- /dev/null +++ b/docs/source/phases/install/mooring/doc_MooringSystemInstallation.rst @@ -0,0 +1,59 @@ +Mooring System Installation Methodology +======================================= + +For details of the code implementation, please see +:doc:`Mooring System Installation API `. + +Overview +-------- + +The ``MooringSystemInstallation`` module simulates the installation of mooring +lines and anchors at site for a floating offshore wind project. The mooring +system installation is simulated using a multi-purpose support vessel that +transports the components to site and performs the onsite installation +procedures. + +Configuration +------------- + +The primary configuration parameters available for this module are the +installation vessel and the mooring system configuration. An example of these +parameters is presented below. + +.. code-block:: python + + config = { + + + "mooring_install_vessel": "example_support_vessel", + "mooring_system": { + "num_lines": 4, # per substructure + "line_mass": 500, # t + "anchor_mass": 500, # t + "anchor_type": "Drag Embedment", # or "Suction Pile" + } + ... + } + +Processes +--------- + +The default times associated with the installation procedure are listed in the +table below. + ++---------------------------+---------------------------------+--------------+ +| Process | Inputs | Default | ++===========================+=================================+==============+ +| Loadout | ``mooring_system_load_time`` | 5h | ++---------------------------+---------------------------------+--------------+ +| Transit | ``vessel.transit_speed`` | calculated | ++---------------------------+---------------------------------+--------------+ +| Survey | ``mooring_site_survey_time`` | 3h | ++---------------------------+---------------------------------+--------------+ +| Install Anchor (repeated) | | ``suction_pile_install_time`` | calculated | +| | | ``drag_embed_install_time`` | | ++---------------------------+---------------------------------+--------------+ +| Install Line (repeated) | NA | calculated | ++---------------------------+---------------------------------+--------------+ +| Transit | ``vessel.transit_speed`` | calculated | ++---------------------------+---------------------------------+--------------+ diff --git a/docs/source/phases/install/quayside_towout/api_GravityBasedInstallation.rst b/docs/source/phases/install/quayside_towout/api_GravityBasedInstallation.rst new file mode 100644 index 00000000..283eda91 --- /dev/null +++ b/docs/source/phases/install/quayside_towout/api_GravityBasedInstallation.rst @@ -0,0 +1,8 @@ +Gravity-Based Foundation Installation API +========================================= + +For detailed methodology, please see +:doc:`Gravity-Based Foundation Installation Methodology `. + +.. autoclass:: ORBIT.phases.install.GravityBasedInstallation + :members: diff --git a/docs/source/phases/install/quayside_towout/api_MooredSubInstallation.rst b/docs/source/phases/install/quayside_towout/api_MooredSubInstallation.rst new file mode 100644 index 00000000..7c7a5f0f --- /dev/null +++ b/docs/source/phases/install/quayside_towout/api_MooredSubInstallation.rst @@ -0,0 +1,8 @@ +Moored Substructure Installation API +==================================== + +For detailed methodology, please see +:doc:`Moored Substructure Installation Methodology `. + +.. autoclass:: ORBIT.phases.install.MooredSubInstallation + :members: diff --git a/docs/source/phases/install/quayside_towout/doc_GravityBasedInstallation.rst b/docs/source/phases/install/quayside_towout/doc_GravityBasedInstallation.rst new file mode 100644 index 00000000..070e1bdb --- /dev/null +++ b/docs/source/phases/install/quayside_towout/doc_GravityBasedInstallation.rst @@ -0,0 +1,10 @@ +Gravity-Based Foundation Installation Methodology +================================================= + +For details of the code implementation, please see +:doc:`Moored Substructure Installation API `. + +Overview +-------- + +This module will be expanded in a future release. diff --git a/docs/source/phases/install/quayside_towout/doc_MooredSubInstallation.rst b/docs/source/phases/install/quayside_towout/doc_MooredSubInstallation.rst new file mode 100644 index 00000000..ad137a51 --- /dev/null +++ b/docs/source/phases/install/quayside_towout/doc_MooredSubInstallation.rst @@ -0,0 +1,93 @@ +Moored Substructure Installation Methodology +============================================ + +For details of the code implementation, please see +:doc:`Moored Substructure Installation API `. + +Overview +-------- + +The ``MooredSubInstallation`` module simulates the manufacture and installation +of moored substuctures for a floating offshore wind project. The installation +procedures include the time required to manufacture a substructure at quayside, +assemble a turbine on the substructure, ballast the completed assembly, tow +the completed assembly to site and hook up the pre-installed moooring lines. + +Configuration +------------- + +The primary configuration parameters available for this module are related to +the quayside assembly process and the vessels used to tow the completed +assemblies to site and complete the installation. The code block highlights +the key parameters available. + +.. code-block:: python + + config = { + + ... + + "support_vessel": "example_support_vessel", # Will perform onsite installation procedures. + "towing_vessel": "example_towing_vessel", # Towing groups will contain multiple of this vessel. + "towing_groups": { + "towing_vessel": 1, # Vessels used to tow the substructure to site. + "station_keeping_vessels": 3, # Vessels used for station keeping during mooring line hookups. + "num_groups": 1 # Number of independent groups. Optional, defualt: 1. + }, + + "port": { + "sub_assembly_lines": 2, # Independent substructure assembly lines. + "sub_storage": 8, # Available storage berths at port for completed substructures. + "turbine_assembly_cranes": 2, # Independent turbine assembly cranes. + "assembly_storage": 8, # Available storage berths at port for completed turbine/substructure assemblies. + }, + + "substructure": { + "takt_time": 168, # h, time to manufacture one substructure. + "towing_speed": 6, # km/h. + }, + + ... + } + + +Processes +--------- + +Quayside Assembly +~~~~~~~~~~~~~~~~~ + ++-------------------------------------------+---------+ +| Process | Default | ++===========================================+=========+ +| Substructure Assembly | 168h | ++-------------------------------------------+---------+ +| Prepare Substructure for Turbine Assembly | 12h | ++-------------------------------------------+---------+ +| Lift and Fasten Tower Section | 12h | +| (repeated if necessary) | | ++-------------------------------------------+---------+ +| Lift and Fasten Nacelle | 7h | ++-------------------------------------------+---------+ +| Lift and Fasten Blade (repeated) | 3.5h | ++-------------------------------------------+---------+ +| Mechanical Completion and Verification | 24h | ++-------------------------------------------+---------+ + + +Substructure Tow-out and Assembly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++-------------------------------------+------------+ +| Process | Default | ++=====================================+============+ +| Ballast to Towing Draft | 6h | ++-------------------------------------+------------+ +| Tow-out | calculated | ++-------------------------------------+------------+ +| Ballast to Operational Draft | 6h | ++-------------------------------------+------------+ +| Connect Mooring Lines | 22h | ++-------------------------------------+------------+ +| Check Mooring Lines and Connections | 12h | ++-------------------------------------+------------+ diff --git a/library/cables/XLPE_1000m_220kV.yaml b/library/cables/XLPE_1000m_220kV.yaml new file mode 100755 index 00000000..62fc48e3 --- /dev/null +++ b/library/cables/XLPE_1000m_220kV.yaml @@ -0,0 +1,9 @@ +ac_resistance: 0.03 # +capacitance: 300 # +conductor_size: 1000 # +cost_per_km: 850000 # +current_capacity: 900 # Guess +inductance: 0.35 # +linear_density: 90 # From BVG Guide to OSW +name: XLPE_1000m_220kV +rated_voltage: 220 diff --git a/library/cables/XLPE_185mm_66kV.yaml b/library/cables/XLPE_185mm_66kV.yaml new file mode 100755 index 00000000..f2371fbd --- /dev/null +++ b/library/cables/XLPE_185mm_66kV.yaml @@ -0,0 +1,9 @@ +ac_resistance: 0.128 # +capacitance: 163 # +conductor_size: 185 # +cost_per_km: 200000 # +current_capacity: 445 # At 2m burial depth +inductance: 0.443 # +linear_density: 26.1 # +name: XLPE_185mm_66kV +rated_voltage: 66 diff --git a/library/cables/XLPE_630mm_66kV.yaml b/library/cables/XLPE_630mm_66kV.yaml new file mode 100755 index 00000000..fa5dcc22 --- /dev/null +++ b/library/cables/XLPE_630mm_66kV.yaml @@ -0,0 +1,9 @@ +ac_resistance: 0.04 +capacitance: 300 +conductor_size: 630 +cost_per_km: 400000 +current_capacity: 775 +inductance: 0.35 +linear_density: 42.5 +name: XLPE_630mm_66kV +rated_voltage: 66 diff --git a/library/project/config/example_array_cable_install.yaml b/library/project/config/example_array_cable_install.yaml index 2c1408af..9230d3fd 100644 --- a/library/project/config/example_array_cable_install.yaml +++ b/library/project/config/example_array_cable_install.yaml @@ -6,6 +6,5 @@ site: distance: 50 port: monthly_rate: 10000 - num_cranes: 1 array_cable_install_vessel: example_cable_lay_vessel array_cable_bury_vessel: example_cable_lay_vessel diff --git a/library/project/config/example_custom_array_project_manager.yaml b/library/project/config/example_custom_array_project_manager.yaml index e3069b4c..3ca16364 100644 --- a/library/project/config/example_custom_array_project_manager.yaml +++ b/library/project/config/example_custom_array_project_manager.yaml @@ -7,7 +7,6 @@ plant: num_turbines: 67 port: monthly_rate: 10000 - num_cranes: 1 site: depth: 20 distance: 50 diff --git a/library/turbines/12MW_generic.yaml b/library/turbines/12MW_generic.yaml new file mode 100755 index 00000000..0a190efb --- /dev/null +++ b/library/turbines/12MW_generic.yaml @@ -0,0 +1,20 @@ +blade: + deck_space: 385 # m^2 + length: 107 # m + type: Blade + mass: 54 # t +hub_height: 132 # m +nacelle: + deck_space: 203 # m^2 + type: Nacelle + mass: 604 # t +name: 12MW Generic Turbine +rated_windspeed: 11 # m/s +rotor_diameter: 215 # m +tower: + deck_space: 50.24 # m^2 + sections: 2 # n + type: Tower + length: 132 + mass: 399 # t +turbine_rating: 12 # MW diff --git a/library/vessels/example_support_vessel.yaml b/library/vessels/example_support_vessel.yaml new file mode 100644 index 00000000..d0ca1b99 --- /dev/null +++ b/library/vessels/example_support_vessel.yaml @@ -0,0 +1,13 @@ +transport_specs: + max_waveheight: 3 # m + max_windspeed: 20 # m/s + transit_speed: 10 # km/h +vessel_specs: + day_rate: 100000 # USD/day + overall_length: 150 # m + mobilization_days: 7 # days + mobilization_mult: 1 # Mobilization multiplier applied to 'day_rate' +storage_specs: + max_cargo: 5000 # t + max_deck_load: 8 # t/m^2 + max_deck_space: 1000 # m^2 diff --git a/library/vessels/example_towing_vessel.yaml b/library/vessels/example_towing_vessel.yaml new file mode 100644 index 00000000..b1de857d --- /dev/null +++ b/library/vessels/example_towing_vessel.yaml @@ -0,0 +1,10 @@ +transport_specs: + max_waveheight: 2.5 # m + max_windspeed: 20 # m/s + transit_speed: 6 # km/h +vessel_specs: + beam_length: 35 # m + day_rate: 30000 # USD/day + max_draft: 5 # m + min_draft: 4 # m + overall_length: 60 # m diff --git a/library/vessels/floating_barge.yaml b/library/vessels/floating_barge.yaml new file mode 100644 index 00000000..0dd7212b --- /dev/null +++ b/library/vessels/floating_barge.yaml @@ -0,0 +1,25 @@ +crane_specs: + max_lift: 500 # t + radius: 15 # m +jacksys_specs: # TEMPORARY UNTIL FLOATING FUNCTIONALY ADDED + air_gap: 10 # m, distance from MSL to jacked up height + leg_length: 85 # m + leg_pen: 5 # m + max_depth: 1000 # m + max_extension: 1000 # m + speed_above_depth: 10000 # m/min + speed_below_depth: 10000 # m/min +storage_specs: + max_cargo: 8000 # t + max_deck_load: 8 # t/m^2 + max_deck_space: 1000 # m^2 +transport_specs: + max_waveheight: 2.5 # m + max_windspeed: 20 # m/s + transit_speed: 6 # km/h +vessel_specs: + beam_length: 35 # m + day_rate: 120000 # USD/day + max_draft: 5 # m + min_draft: 4 # m + overall_length: 60 # m diff --git a/library/vessels/floating_heavy_lift_vessel.yaml b/library/vessels/floating_heavy_lift_vessel.yaml new file mode 100644 index 00000000..fea7e5da --- /dev/null +++ b/library/vessels/floating_heavy_lift_vessel.yaml @@ -0,0 +1,26 @@ +crane_specs: + boom_length: 100 # m + max_hook_height: 72 # m + max_lift: 5500 # t + max_windspeed: 15 # m/s + radius: 30 # m +jacksys_specs: # TEMPORARY UNTIL FLOATING FUNCTIONALY ADDED + air_gap: 10 # m, distance from MSL to jacked up height + leg_length: 110 # m + leg_pen: 5 # m + max_depth: 1000 # m + max_extension: 1000 # m + speed_above_depth: 10000 # m/min + speed_below_depth: 10000 # m/min +storage_specs: + max_cargo: 8000 # t + max_deck_load: 15 # t/m^2 + max_deck_space: 4000 # m^2 +transport_specs: + max_waveheight: 2.5 # m + max_windspeed: 20 # m/s + transit_speed: 7 # km/h +vessel_specs: + day_rate: 500000 # USD/day + max_draft: 4.5 # m + overall_length: 102.75 # m diff --git a/tests/data/library/project/config/complete_project.yaml b/tests/data/library/project/config/complete_project.yaml index 2c6898f9..5a29b68a 100644 --- a/tests/data/library/project/config/complete_project.yaml +++ b/tests/data/library/project/config/complete_project.yaml @@ -40,8 +40,6 @@ plant: row_spacing: 7 substation_distance: 1 turbine_spacing: 7 -port: - num_cranes: 1 scour_protection_design: cost_per_tonne: 40 scour_protection_depth: 1 diff --git a/tests/data/library/project/config/gbf_install.yaml b/tests/data/library/project/config/gbf_install.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/library/project/config/library.yaml b/tests/data/library/project/config/library.yaml index 77996402..e844eac2 100644 --- a/tests/data/library/project/config/library.yaml +++ b/tests/data/library/project/config/library.yaml @@ -15,7 +15,6 @@ plant: num_turbines: 10 port: monthly_rate: 10000 - num_cranes: 1 site: depth: 15 distance: 100 diff --git a/tests/data/library/project/config/moored_install.yaml b/tests/data/library/project/config/moored_install.yaml new file mode 100644 index 00000000..90b0b5ff --- /dev/null +++ b/tests/data/library/project/config/moored_install.yaml @@ -0,0 +1,20 @@ +plant: + num_turbines: 50 +port: + assembly_storage: 1 + sub_assembly_lines: 1 + sub_storage: 1 + turbine_assembly_cranes: 1 +site: + depth: 500 + distance: 50 +substructure: + takt_time: 168 + towing_speed: 6 +support_vessel: test_support_vessel +towing_vessel: test_towing_vessel +towing_vessel_groups: + num_groups: 1 + station_keeping_vessels: 3 + towing_vessels: 1 +turbine: 12MW_generic diff --git a/tests/data/library/project/config/moored_install_no_supply.yaml b/tests/data/library/project/config/moored_install_no_supply.yaml new file mode 100644 index 00000000..7a0782a2 --- /dev/null +++ b/tests/data/library/project/config/moored_install_no_supply.yaml @@ -0,0 +1,18 @@ +plant: + num_turbines: 50 +port: + assembly_storage: 1 + turbine_assembly_cranes: 1 +site: + depth: 500 + distance: 50 +substructure: + takt_time: 0 + towing_speed: 6 +support_vessel: test_support_vessel +towing_vessel: test_towing_vessel +towing_vessel_groups: + num_groups: 1 + station_keeping_vessels: 3 + towing_vessels: 1 +turbine: 12MW_generic diff --git a/tests/data/library/project/config/mooring_system_install.yaml b/tests/data/library/project/config/mooring_system_install.yaml new file mode 100644 index 00000000..a9e34a60 --- /dev/null +++ b/tests/data/library/project/config/mooring_system_install.yaml @@ -0,0 +1,10 @@ +plant: + num_turbines: 50 +mooring_install_vessel: test_support_vessel +site: + depth: 40 + distance: 30 +mooring_system: + num_lines: 3 + line_mass: 500 + anchor_mass: 100 diff --git a/tests/data/library/project/config/project_manager.yaml b/tests/data/library/project/config/project_manager.yaml index ce5b22e4..47187b96 100644 --- a/tests/data/library/project/config/project_manager.yaml +++ b/tests/data/library/project/config/project_manager.yaml @@ -15,7 +15,6 @@ plant: turbine_spacing: 7 port: monthly_rate: 10000 - num_cranes: 1 site: depth: 15 distance: 50 diff --git a/tests/data/library/project/config/scour_protection_install.yaml b/tests/data/library/project/config/scour_protection_install.yaml index 120a96d6..fd23223c 100644 --- a/tests/data/library/project/config/scour_protection_install.yaml +++ b/tests/data/library/project/config/scour_protection_install.yaml @@ -3,7 +3,6 @@ plant: turbine_spacing: 5 port: monthly_rate: 100000 - num_cranes: 1 scour_protection: tons_per_substructure: 2000 spi_vessel: test_scour_protection_vessel diff --git a/tests/data/library/turbines/12MW_generic.yaml b/tests/data/library/turbines/12MW_generic.yaml new file mode 100755 index 00000000..0a190efb --- /dev/null +++ b/tests/data/library/turbines/12MW_generic.yaml @@ -0,0 +1,20 @@ +blade: + deck_space: 385 # m^2 + length: 107 # m + type: Blade + mass: 54 # t +hub_height: 132 # m +nacelle: + deck_space: 203 # m^2 + type: Nacelle + mass: 604 # t +name: 12MW Generic Turbine +rated_windspeed: 11 # m/s +rotor_diameter: 215 # m +tower: + deck_space: 50.24 # m^2 + sections: 2 # n + type: Tower + length: 132 + mass: 399 # t +turbine_rating: 12 # MW diff --git a/tests/data/library/vessels/test_support_vessel.yaml b/tests/data/library/vessels/test_support_vessel.yaml new file mode 100644 index 00000000..d0ca1b99 --- /dev/null +++ b/tests/data/library/vessels/test_support_vessel.yaml @@ -0,0 +1,13 @@ +transport_specs: + max_waveheight: 3 # m + max_windspeed: 20 # m/s + transit_speed: 10 # km/h +vessel_specs: + day_rate: 100000 # USD/day + overall_length: 150 # m + mobilization_days: 7 # days + mobilization_mult: 1 # Mobilization multiplier applied to 'day_rate' +storage_specs: + max_cargo: 5000 # t + max_deck_load: 8 # t/m^2 + max_deck_space: 1000 # m^2 diff --git a/tests/data/library/vessels/test_towing_vessel.yaml b/tests/data/library/vessels/test_towing_vessel.yaml new file mode 100644 index 00000000..b1de857d --- /dev/null +++ b/tests/data/library/vessels/test_towing_vessel.yaml @@ -0,0 +1,10 @@ +transport_specs: + max_waveheight: 2.5 # m + max_windspeed: 20 # m/s + transit_speed: 6 # km/h +vessel_specs: + beam_length: 35 # m + day_rate: 30000 # USD/day + max_draft: 5 # m + min_draft: 4 # m + overall_length: 60 # m diff --git a/tests/phases/design/test_array_system_design.py b/tests/phases/design/test_array_system_design.py index e71f55a5..e464c5e6 100644 --- a/tests/phases/design/test_array_system_design.py +++ b/tests/phases/design/test_array_system_design.py @@ -206,3 +206,36 @@ def test_correct_turbines(): with pytest.raises(ValueError): array.run() + + +def test_floating_calculations(): + + base = deepcopy(config_full_ring) + base["site"]["depth"] = 50 + number = base["plant"]["num_turbines"] + + sim = ArraySystemDesign(base) + sim.run() + + base_length = sim.total_length + + floating_no_cat = deepcopy(base) + floating_no_cat["site"]["depth"] = 250 + floating_no_cat["array_system_design"]["touchdown_distance"] = 0 + + sim2 = ArraySystemDesign(floating_no_cat) + sim2.run() + + no_cat_length = sim2.total_length + assert no_cat_length == pytest.approx( + base_length + 2 * (200 / 1000) * number + ) + + floating_cat = deepcopy(base) + floating_cat["site"]["depth"] = 250 + + sim3 = ArraySystemDesign(floating_cat) + sim3.run() + + with_cat_length = sim3.total_length + assert with_cat_length < no_cat_length diff --git a/tests/phases/design/test_export_system_design.py b/tests/phases/design/test_export_system_design.py index c8a8565d..76ffcae0 100644 --- a/tests/phases/design/test_export_system_design.py +++ b/tests/phases/design/test_export_system_design.py @@ -5,6 +5,7 @@ __maintainer__ = "Rob Hammond" __email__ = "robert.hammond@nrel.gov" +from copy import deepcopy import pytest @@ -100,3 +101,23 @@ def test_design_result(): assert cables["sections"] == [export.length] assert cables["number"] == 11 assert cables["linear_density"] == export.cable.linear_density + + +def test_floating_length_calculations(): + + base = deepcopy(config) + base["site"]["depth"] = 250 + base["export_system_design"]["touchdown_distance"] = 0 + + sim = ExportSystemDesign(base) + sim.run() + + base_length = sim.total_length + + with_cat = deepcopy(config) + with_cat["site"]["depth"] = 250 + + new = ExportSystemDesign(with_cat) + new.run() + + assert new.total_length < base_length diff --git a/tests/phases/design/test_mooring_system_design.py b/tests/phases/design/test_mooring_system_design.py new file mode 100644 index 00000000..39e2cb9c --- /dev/null +++ b/tests/phases/design/test_mooring_system_design.py @@ -0,0 +1,85 @@ +"""Tests for the `MooringSystemDesign` class.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +from copy import deepcopy + +import pytest + +from ORBIT.phases.design import MooringSystemDesign + +base = { + "site": {"depth": 200}, + "turbine": {"turbine_rating": 6}, + "plant": {"num_turbines": 50}, +} + + +@pytest.mark.parametrize("depth", range(10, 1000, 100)) +def test_depth_sweep(depth): + + config = deepcopy(base) + config["site"]["depth"] = depth + + m = MooringSystemDesign(config) + m.run() + + assert m.design_result + assert m.total_phase_cost + + +@pytest.mark.parametrize("rating", range(3, 15, 1)) +def test_rating_sweeip(rating): + + config = deepcopy(base) + config["turbine"]["turbine_rating"] = rating + + m = MooringSystemDesign(config) + m.run() + + assert m.design_result + assert m.total_phase_cost + + +def test_drag_embedment_fixed_length(): + + m = MooringSystemDesign(base) + m.run() + + baseline = m.line_length + + default = deepcopy(base) + default["mooring_system_design"] = {"anchor_type": "Drag Embedment"} + + m = MooringSystemDesign(default) + m.run() + + with_default = m.line_length + assert with_default > baseline + + custom = deepcopy(base) + custom["mooring_system_design"] = { + "anchor_type": "Drag Embedment", + "drag_embedment_fixed_length": 10, + } + + m = MooringSystemDesign(custom) + m.run() + + assert m.line_length > with_default + assert m.line_length > baseline + + +def test_custom_num_lines(): + + config = deepcopy(base) + config["mooring_system_design"] = {"num_lines": 5} + + m = MooringSystemDesign(config) + m.run() + + assert m.design_result["mooring_system"]["num_lines"] == 5 diff --git a/tests/phases/design/test_semisubmersible_design.py b/tests/phases/design/test_semisubmersible_design.py new file mode 100644 index 00000000..de4315e2 --- /dev/null +++ b/tests/phases/design/test_semisubmersible_design.py @@ -0,0 +1,66 @@ +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "Jake.Nunemaker@nrel.gov" + + +from copy import deepcopy +from itertools import product + +import pytest + +from ORBIT.phases.design import SemiSubmersibleDesign + +base = { + "site": {"depth": 500}, + "plant": {"num_turbines": 50}, + "turbine": {"turbine_rating": 12}, + "semisubmersible_design": {}, +} + + +@pytest.mark.parametrize( + "depth,turbine_rating", product(range(100, 1201, 200), range(3, 15, 1)) +) +def test_parameter_sweep(depth, turbine_rating): + + config = { + "site": {"depth": depth}, + "plant": {"num_turbines": 50}, + "turbine": {"turbine_rating": turbine_rating}, + "substation_design": {}, + } + + s = SemiSubmersibleDesign(config) + s.run() + + assert s.detailed_output["stiffened_column_mass"] > 0 + assert s.detailed_output["truss_mass"] > 0 + assert s.detailed_output["heave_plate_mass"] > 0 + assert s.detailed_output["secondary_steel_mass"] > 0 + + +def test_design_kwargs(): + + test_kwargs = { + "stiffened_column_CR": 3000, + "truss_CR": 6000, + "heave_plate_CR": 6000, + "secondary_steel_CR": 7000, + } + + s = SemiSubmersibleDesign(base) + s.run() + base_cost = s.total_phase_cost + + for k, v in test_kwargs.items(): + + config = deepcopy(base) + config["semisubmersible_design"] = {} + config["semisubmersible_design"][k] = v + + s = SemiSubmersibleDesign(config) + s.run() + cost = s.total_phase_cost + + assert cost != base_cost diff --git a/tests/phases/design/test_spar_design.py b/tests/phases/design/test_spar_design.py new file mode 100644 index 00000000..f21bd2a2 --- /dev/null +++ b/tests/phases/design/test_spar_design.py @@ -0,0 +1,66 @@ +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "Jake.Nunemaker@nrel.gov" + + +from copy import deepcopy +from itertools import product + +import pytest + +from ORBIT.phases.design import SparDesign + +base = { + "site": {"depth": 500}, + "plant": {"num_turbines": 50}, + "turbine": {"turbine_rating": 12}, + "spar_design": {}, +} + + +@pytest.mark.parametrize( + "depth,turbine_rating", product(range(100, 1201, 200), range(3, 15, 1)) +) +def test_parameter_sweep(depth, turbine_rating): + + config = { + "site": {"depth": depth}, + "plant": {"num_turbines": 50}, + "turbine": {"turbine_rating": turbine_rating}, + "substation_design": {}, + } + + s = SparDesign(config) + s.run() + + assert s.detailed_output["stiffened_column_mass"] > 0 + assert s.detailed_output["tapered_column_mass"] > 0 + assert s.detailed_output["ballast_mass"] > 0 + assert s.detailed_output["secondary_steel_mass"] > 0 + + +def test_design_kwargs(): + + test_kwargs = { + "stiffened_column_CR": 3000, + "tapered_column_CR": 4000, + "ballast_material_CR": 200, + "secondary_steel_CR": 7000, + } + + s = SparDesign(base) + s.run() + base_cost = s.total_phase_cost + + for k, v in test_kwargs.items(): + + config = deepcopy(base) + config["spar_design"] = {} + config["spar_design"][k] = v + + s = SparDesign(config) + s.run() + cost = s.total_phase_cost + + assert cost != base_cost diff --git a/tests/phases/install/cable_install/test_export_install.py b/tests/phases/install/cable_install/test_export_install.py index 31b6a004..6da9258f 100644 --- a/tests/phases/install/cable_install/test_export_install.py +++ b/tests/phases/install/cable_install/test_export_install.py @@ -122,7 +122,7 @@ def test_kwargs_for_export_install(): new_export_system = { "cable": {"linear_density": 50.0, "sections": [1000], "number": 1} } - new_site = {"distance": 50} + new_site = {"distance": 50, "depth": 20} new_config = deepcopy(base_config) new_config["export_system"] = new_export_system diff --git a/tests/phases/install/mooring_install/test_mooring_install.py b/tests/phases/install/mooring_install/test_mooring_install.py new file mode 100644 index 00000000..48570cfc --- /dev/null +++ b/tests/phases/install/mooring_install/test_mooring_install.py @@ -0,0 +1,94 @@ +""" +Testing framework for the `MooringSystemInstallation` class. +""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "Jake.Nunemaker@nrel.gov" + + +from copy import deepcopy + +import pandas as pd +import pytest + +from tests.data import test_weather +from ORBIT.library import extract_library_specs +from ORBIT.core._defaults import process_times as pt +from ORBIT.phases.install import MooringSystemInstallation + +config = extract_library_specs("config", "mooring_system_install") + + +def test_simulation_creation(): + sim = MooringSystemInstallation(config) + + assert sim.config == config + assert sim.env + assert sim.port + assert sim.vessel + assert sim.number_systems + + +@pytest.mark.parametrize( + "weather", (None, test_weather), ids=["no_weather", "test_weather"] +) +def test_full_run_logging(weather): + sim = MooringSystemInstallation(config, weather=weather) + sim.run() + + lines = ( + config["plant"]["num_turbines"] * config["mooring_system"]["num_lines"] + ) + + df = pd.DataFrame(sim.env.actions) + df = df.assign(shift=(df.time - df.time.shift(1))) + assert (df.duration - df["shift"]).fillna(0.0).abs().max() < 1e-9 + assert df[df.action == "Install Mooring Line"].shape[0] == lines + + assert ~df["cost"].isnull().any() + _ = sim.agent_efficiencies + _ = sim.detailed_output + + +@pytest.mark.parametrize( + "anchor, key", + [ + ("Suction Pile", "suction_pile_install_time"), + ("Drag Embedment", "drag_embed_install_time"), + ], +) +def test_kwargs(anchor, key): + + new = deepcopy(config) + new["mooring_system"]["anchor_type"] = anchor + + sim = MooringSystemInstallation(new) + sim.run() + baseline = sim.total_phase_time + + keywords = ["mooring_system_load_time", "mooring_site_survey_time", key] + + failed = [] + + for kw in keywords: + + default = pt[kw] + kwargs = {kw: default + 2} + + new_sim = MooringSystemInstallation(new, **kwargs) + new_sim.run() + new_time = new_sim.total_phase_time + + if new_time > baseline: + pass + + else: + failed.append(kw) + + if failed: + raise Exception(f"'{failed}' not affecting results.") + + else: + assert True diff --git a/tests/phases/install/quayside_assembly_tow/test_common.py b/tests/phases/install/quayside_assembly_tow/test_common.py new file mode 100644 index 00000000..00fa2b4e --- /dev/null +++ b/tests/phases/install/quayside_assembly_tow/test_common.py @@ -0,0 +1,117 @@ +"""Tests for common infrastructure for quayside assembly tow-out simulations""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +import pandas as pd +import pytest + +from ORBIT.core import WetStorage +from ORBIT.phases.install.quayside_assembly_tow.common import ( + TurbineAssemblyLine, + SubstructureAssemblyLine, +) + + +@pytest.mark.parametrize( + "num, assigned, expected", + [ + (1, [], 0), + (1, [1] * 10, 100), + (2, [1] * 10, 50), + (3, [1] * 10, 40), + (5, [1] * 10, 20), + (10, [1] * 10, 10), + ], +) +def test_SubstructureAssemblyLine(env, num, assigned, expected): + + _assigned = len(assigned) + storage = WetStorage(env, capacity=float("inf")) + + for a in range(num): + assembly = SubstructureAssemblyLine(assigned, 10, storage, a + 1) + env.register(assembly) + assembly.start() + + env.run() + + assert len(env.actions) == _assigned + assert env.now == expected + + +@pytest.mark.parametrize( + "num, assigned", + [ + (1, [1] * 10), + (2, [1] * 10), + (3, [1] * 10), + (5, [1] * 10), + (10, [1] * 10), + ], +) +def test_TurbineAssemblyLine(env, num, assigned): + + _assigned = len(assigned) + feed = WetStorage(env, capacity=float("inf")) + target = WetStorage(env, capacity=float("inf")) + + for i in assigned: + feed.put(0) + + for a in range(num): + assembly = TurbineAssemblyLine( + feed, target, {"tower": {"sections": 1}}, a + 1 + ) + env.register(assembly) + assembly.start() + + env.run() + + df = pd.DataFrame(env.actions) + assert len(df.loc[df["action"] == "Mechanical Completion"]) == len( + assigned + ) + + +@pytest.mark.parametrize( + "sub_lines, turb_lines", + [ + (1, 1), + (1, 10), + (1, 100), + (10, 1), + (10, 10), + (10, 100), + (100, 1), + (100, 10), + (100, 100), + ], +) +def test_Sub_to_Turbine_assembly_interaction(env, sub_lines, turb_lines): + + num_turbines = 50 + assigned = [1] * num_turbines + + feed = WetStorage(env, capacity=2) + target = WetStorage(env, capacity=float("inf")) + + for a in range(sub_lines): + assembly = SubstructureAssemblyLine(assigned, 10, feed, a + 1) + env.register(assembly) + assembly.start() + + for a in range(turb_lines): + assembly = TurbineAssemblyLine( + feed, target, {"tower": {"sections": 1}}, a + 1 + ) + env.register(assembly) + assembly.start() + + env.run() + + df = pd.DataFrame(env.actions) + assert len(df.loc[df["action"] == "Mechanical Completion"]) == num_turbines diff --git a/tests/phases/install/quayside_assembly_tow/test_gravity_based.py b/tests/phases/install/quayside_assembly_tow/test_gravity_based.py new file mode 100644 index 00000000..0d0eb67c --- /dev/null +++ b/tests/phases/install/quayside_assembly_tow/test_gravity_based.py @@ -0,0 +1,58 @@ +"""Tests for the `GravityBasedInstallation` class and related infrastructure.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +import pandas as pd +import pytest + +from tests.data import test_weather +from ORBIT.library import extract_library_specs +from ORBIT.phases.install import GravityBasedInstallation + +config = extract_library_specs("config", "moored_install") +no_supply = extract_library_specs("config", "moored_install_no_supply") + + +def test_simulation_setup(): + + sim = GravityBasedInstallation(config) + assert sim.config == config + assert sim.env + + assert sim.support_vessel + assert len(sim.sub_assembly_lines) == config["port"]["sub_assembly_lines"] + assert ( + len(sim.turbine_assembly_lines) + == config["port"]["turbine_assembly_cranes"] + ) + assert ( + len(sim.installation_groups) + == config["towing_vessel_groups"]["num_groups"] + ) + assert sim.num_turbines == config["plant"]["num_turbines"] + + +@pytest.mark.parametrize( + "weather", (None, test_weather), ids=["no_weather", "test_weather"] +) +@pytest.mark.parametrize("config", (config, no_supply)) +def test_for_complete_logging(weather, config): + + sim = GravityBasedInstallation(config, weather=weather) + sim.run() + + df = pd.DataFrame(sim.env.actions) + df = df.assign(shift=(df["time"] - df["time"].shift(1))) + + for vessel in df["agent"].unique(): + _df = df[df["agent"] == vessel].copy() + _df = _df.assign(shift=(_df["time"] - _df["time"].shift(1))) + assert (_df["shift"] - _df["duration"]).abs().max() < 1e-9 + + assert ~df["cost"].isnull().any() + _ = sim.agent_efficiencies + _ = sim.detailed_output diff --git a/tests/phases/install/quayside_assembly_tow/test_moored.py b/tests/phases/install/quayside_assembly_tow/test_moored.py new file mode 100644 index 00000000..e87ddb3b --- /dev/null +++ b/tests/phases/install/quayside_assembly_tow/test_moored.py @@ -0,0 +1,58 @@ +"""Tests for the `MooredSubInstallation` class and related infrastructure.""" + +__author__ = "Jake Nunemaker" +__copyright__ = "Copyright 2020, National Renewable Energy Laboratory" +__maintainer__ = "Jake Nunemaker" +__email__ = "jake.nunemaker@nrel.gov" + + +import pandas as pd +import pytest + +from tests.data import test_weather +from ORBIT.library import extract_library_specs +from ORBIT.phases.install import MooredSubInstallation + +config = extract_library_specs("config", "moored_install") +no_supply = extract_library_specs("config", "moored_install_no_supply") + + +def test_simulation_setup(): + + sim = MooredSubInstallation(config) + assert sim.config == config + assert sim.env + + assert sim.support_vessel + assert len(sim.sub_assembly_lines) == config["port"]["sub_assembly_lines"] + assert ( + len(sim.turbine_assembly_lines) + == config["port"]["turbine_assembly_cranes"] + ) + assert ( + len(sim.installation_groups) + == config["towing_vessel_groups"]["num_groups"] + ) + assert sim.num_turbines == config["plant"]["num_turbines"] + + +@pytest.mark.parametrize( + "weather", (None, test_weather), ids=["no_weather", "test_weather"] +) +@pytest.mark.parametrize("config", (config, no_supply)) +def test_for_complete_logging(weather, config): + + sim = MooredSubInstallation(config, weather=weather) + sim.run() + + df = pd.DataFrame(sim.env.actions) + df = df.assign(shift=(df["time"] - df["time"].shift(1))) + + for vessel in df["agent"].unique(): + _df = df[df["agent"] == vessel].copy() + _df = _df.assign(shift=(_df["time"] - _df["time"].shift(1))) + assert (_df["shift"] - _df["duration"]).abs().max() < 1e-9 + + assert ~df["cost"].isnull().any() + _ = sim.agent_efficiencies + _ = sim.detailed_output