ProjectManager Deep Dive#

ProjectManager is the primary system for interacting with ORBIT. It provides the ability to configure and run one or multiple models at a time, allowing the user to customize ORBIT to fit the needs of a specific project.

from pathlib import Path
from pprint import pprint

import pandas as pd
import matplotlib.pyplot as plt

from ORBIT import ProjectManager

# Ensure the correct examples directory is used when running this in docs or in examples
here = Path(".").resolve()
match here.stem:
    case "examples":
        example_dir = here
    case "tutorials":
        example_dir = here.parents[1] / "examples"
    case "ORBIT":
        example_dir = here / "examples"
    case _:
        msg = "Please manually change `example_dir` if running in a custom location."
        raise FileNotFoundError(msg)

Compiling Input Requirements Dynamically#

To better understand the input requirements for designing and installing multiple turbine subsystems, ProjectManager provides the compile_input_dict() method that will generate the expected configuration of each provided phase in a single configuration dictionary. The example below shows how to configure a simple project with a design and multiple installation phases, and return the required configuration parameters.

phases = [
    "MonopileDesign",
    "MonopileInstallation",
    "TurbineInstallation",
]

expected_config = ProjectManager.compile_input_dict(phases)
pprint(expected_config)
{'design_phases': ['MonopileDesign'],
 'feeder': 'dict | str (optional)',
 'install_phases': ['MonopileInstallation', 'TurbineInstallation'],
 'monopile_design': {'air_density': 'kg/m3 (optional)',
                     'load_factor': 'float (optional)',
                     'material_factor': 'float (optional)',
                     'monopile_density': 'kg/m3 (optional)',
                     'monopile_modulus': 'Pa (optional)',
                     'monopile_steel_cost': 'USD/t (optional)',
                     'monopile_tp_connection_thickness': 'm (optional)',
                     'soil_coefficient': 'N/m3 (optional)',
                     'tp_steel_cost': 'USD/t (optional)',
                     'transition_piece_density': 'kg/m3 (optional)',
                     'transition_piece_length': 'm (optional)',
                     'transition_piece_thickness': 'm (optional)',
                     'turb_length_scale': 'm (optional)',
                     'weibull_scale_factor': 'float (optional)',
                     'weibull_shape_factor': 'float (optional)',
                     'yield_stress': 'Pa (optional)'},
 'monopile_supply_chain': {'enabled': '(optional, default: False)',
                           'num_substructures_delivered': 'int (optional: '
                                                          'default: 1)',
                           'substructure_delivery_time': 'h (optional, '
                                                         'default: 168)'},
 'num_feeders': 'int (optional)',
 'orbit_version': '1.3',
 'plant': {'num_turbines': 'int'},
 'port': {'monthly_rate': 'USD/mo (optional)',
          'name': 'str (optional)',
          'num_cranes': 'int (optional, default: 1)'},
 'project_parameters': {'commissioning': '$/kW (optional, default: value '
                                         'calculated using '
                                         'commissioning_factor)',
                        'commissioning_factor': 'float (optional, default: '
                                                '0.0115)',
                        'construction_financing': '$/kW (optional, default: '
                                                  'value calculated using '
                                                  'construction_financing_factor))',
                        'construction_financing_factor': ('$/kW (optional, '
                                                          'default: value '
                                                          'calculated using '
                                                          'spend_schedule, '
                                                          'tax_rate, and '
                                                          'interest_during_construction))',),
                        'construction_insurance': '$/kW (optional, default: '
                                                  'value calculated using '
                                                  'construction_insurance_factor)',
                        'construction_insurance_factor': 'float (optional, '
                                                         'default: 0.0207)',
                        'construction_plan_cost': '$ (optional, default: 25e6)',
                        'decommissioning': '$/kW (optional, default: value '
                                           'calculated using '
                                           'decommissioning_factor)',
                        'decommissioning_factor': 'float (optional, default: '
                                                  '0.2)',
                        'discount_rate': 'yearly (optional, default: .025)',
                        'installation_contingency': '$/kW (optional, default: '
                                                    'value calculated using '
                                                    'installation_contingency_factor)',
                        'installation_contingency_factor': 'float (optional, '
                                                           'default: 0.345)',
                        'installation_plan_cost': '$ (optional, default: 25e6)',
                        'interest_during_construction': 'float (optional, '
                                                        'default: 0.065',
                        'ncf': 'float (optional, default: 0.4)',
                        'offtake_price': '$/MWh (optional, default: 80)',
                        'opex_rate': '$/kW/year (optional, default: 150)',
                        'procurement_contingency': '$/kW (optional, default: '
                                                   'value calculated using '
                                                   'procurement_contingency_factor)',
                        'procurement_contingency_factor': 'float (optional, '
                                                          'default: 0.0575)',
                        'project_lifetime': 'yrs (optional, default: 25)',
                        'site_assessment_cost': '$ (optional, default: 200e6)',
                        'site_auction_price': '$ (optional, default: 105e6)',
                        'spend_schedule': 'dict (optional, default: {0: 0.25, '
                                          '1: 0.25, 2: 0.3, 3: 0.1, 4: 0.1, 5: '
                                          '0.0}',
                        'tax_rate': 'float (optional, default: 0.26',
                        'turbine_capex': '$/kW (optional, default: 1300)'},
 'site': {'depth': 'm', 'distance': 'km', 'mean_windspeed': 'm/s'},
 'turbine': {'blade': {'deck_space': 'm2', 'mass': 't'},
             'hub_height': 'm',
             'nacelle': {'deck_space': 'm2', 'mass': 't'},
             'rated_windspeed': 'm/s',
             'rotor_diameter': 'm',
             'tower': {'deck_space': 'm2',
                       'length': 'm',
                       'mass': 't',
                       'sections': 'int (optional)'}},
 'wtiv': 'dict | str'}

Using the results of the expected_config, the following configuration is now created to minimally define a project running only the monopile phases for design and installation, and the turbine installation phase. Note that the turbine is a copy of the 12MW generic turbine from ORBIT library.

config = {
    "site": {
        "depth": 20,
        "distance": 50,
        "mean_windspeed": 9.5,
    },
    "plant": {
        "num_turbines": 50,
    },
    "turbine": {
        "name": "12MW Generic Turbine",
        "rotor_diameter": 205,
        "hub_height": 125,
        "rated_windspeed": 11,
        "blade": {
            "deck_space": 385,
            "length": 107,
            "type": "Blade",
            "mass": 54,
        },
        "nacelle": {
            "deck_space": 203,
            "type": "Nacelle",
            "mass": 604,
        },
        "tower": {
            "deck_space": 50.24,
            "sections": 2,
            "type": "Tower",
            "length": 132,
            "mass": 399,
        },
    },
    "wtiv": "example_wtiv",
    "design_phases": ["MonopileDesign"],
    "install_phases": ["MonopileInstallation", "TurbineInstallation"],
}

project = ProjectManager(config)
project.run()
ORBIT library intialized at '/opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/library'

Weather Profiles#

To include wind and wave conditions in the simulation for vessel and port constraints, pass an hourly pandas DataFrame to ProjectManager using the weather keyword argument. All installation phases will now use this time series to account for weather delays.

weather = pd.read_csv(example_dir / "data/example_weather.csv").set_index("datetime")

project = ProjectManager(config, weather=weather)
project.run()

Accessing Individual Models#

The ProjectManager provides a dictionary-based attribute phases that allows users to access the design or installation class for custom results gathering or model inspection. Using the previously run project, we now directly access the monopile design costs.

monopile_design_cost = project.phases["MonopileDesign"].total_cost
print(f"Total Monopile Cost: ${monopile_design_cost / 1e6:,.2f} M")
Total Monopile Cost: $335.34 M

Phase-Specific Configurations#

As was seen in inputs compilation demonstration, ProjectManager compiles the minimum required configuration, combining the same parameter that is needed for multiple phases into one input. This isn't always a desired outcome as there are cases when inputs need to be different for each phase. For example, the distance_to_shore parameter may be different for each installation phase if different ports are used to stage monopiles and turbines or the installations may use different installation vessels.

In these cases, it is necessary to define phase specific input parameters using the phase's name as the dictionary key. Below, we can see how we model a differing staging port where a separate WTIV will be used with its much further port distance. Please note that the turbine installation's WTIV "other_wtiv" is not a valid vessel configuration file, so this demonstration setup will fail if used.

Please note that phase-specific configurations will always override their general counterparts.

config = {
    "site": {
        "depth": 20,
        "distance": 50,
        "mean_windspeed": 9.5,
    },
    "plant": {
        "num_turbines": 50,
    },
    "turbine": {
        "name": "12MW Generic Turbine",
        "rotor_diameter": 205,
        "hub_height": 125,
        "rated_windspeed": 11,
        "blade": {
            "deck_space": 385,
            "length": 107,
            "type": "Blade",
            "mass": 54,
        },
        "nacelle": {
            "deck_space": 203,
            "type": "Nacelle",
            "mass": 604,
        },
        "tower": {
            "deck_space": 50.24,
            "sections": 2,
            "type": "Tower",
            "length": 132,
            "mass": 399,
        },
    },
    "TurbineInstallation": {
        "wtiv": "other_wtiv",
        "site": {
            "distance": 100,
        },
    },
    "wtiv": "example_wtiv",
    "design_phases": ["MonopileDesign"],
    "install_phases": ["MonopileInstallation", "TurbineInstallation"],
}

Phase Timing#

By default, all phases will run in the order they are defined in both the design_phases and install_phases. When a weather profile is provided, all phases will start at the beginning of the weather profile. To more realistically simulate the timing of installations, phase start dates can be customized to start at a specific date, or be reliant on the completion status of a dependent phase. The next two subsections will detail how both of these work, and can be used together.

Warning

ORBIT does not have any safety mechanisms to avoid inappropriate installation overlaps, i.e., installing turbines before the monopiles have been fully installed, so it is important to check the installation timing to ensure unrealistic conditions have not been modeled.

Defining Start Dates#

Instead of defining the install_phases as a list of strings for each phase, a dictionary of the phase's class name and the string starting date should be provided. In the following example configuration (derived from examples/configs/example_fixed_project.yaml) for a complete wind power plant, we can see how each of the phases are staggered based on an imagined idealized starting date.

config = {
    "design_phases": [
        "MonopileDesign",
        "ScourProtectionDesign",
        "ArraySystemDesign",
        "ExportSystemDesign",
        "OffshoreSubstationDesign",
    ],
    "install_phases": {
        "MonopileInstallation": "04/01/2020",
        "TurbineInstallation": "05/01/2020",
        "ArrayCableInstallation": "08/01/2020",
        "OffshoreSubstationInstallation": "08/01/2020",
        "ScourProtectionInstallation": "03/01/2021",
        "ExportCableInstallation": "08/15/2020",
    },
    "turbine": "12MW_generic",
    "project_parameters": {"turbine_capex": 1500},
    "site": {
        "depth": 22.5,
        "distance": 124,
        "distance_to_landfall": 35,
        "mean_windspeed": 9,
    },
    "plant": {
        "layout": "grid",
        "num_turbines": 50,
        "row_spacing": 7,
        "substation_distance": 1,
        "turbine_spacing": 7,
    },
    "array_system_design": {"cables": ["XLPE_630mm_33kV", "XLPE_400mm_33kV"]},
    "export_system_design": {
        "cables": "XLPE_500mm_132kV",
        "percent_added_length": 0.0,
        "landfall": {"interconnection_distance": 3, "trench_length": 2},
    },
    "scour_protection_design": {"cost_per_tonne": 40, "scour_protection_depth": 1},
    "OffshoreSubstationInstallation": {
        "feeder": "example_heavy_feeder",
        "num_feeders": 1,
    },
    "wtiv": "example_wtiv",
    "feeder": "example_heavy_feeder",
    "num_feeders": 2,
    "spi_vessel": "example_scour_protection_vessel",
    "oss_install_vessel": "example_heavy_lift_vessel",
    "array_cable_install_vessel": "example_cable_lay_vessel",
    "export_cable_bury_vessel": "example_cable_lay_vessel",
    "export_cable_install_vessel": "example_cable_lay_vessel",
}

project = ProjectManager(config)
project.run()

Now, we can make a quick visualization to see how the start timing plays out. Notice how the monopile and turbine installations overlap yet there is only a single WTIV assigned to the site. In practice this should not be possible, but is helpful to highlight why care is needed when configuring phase timing.

df = pd.DataFrame.from_dict(project.phase_dates).T
df.start = pd.to_datetime(df.start)
df.end = pd.to_datetime(df.end)
df.sort_values("start")

fig = plt.figure(figsize=(10, 4))
ax = fig.add_subplot(111)

ax.barh(y=df.index, width=df.end - df.start, left=df.start);

ax.grid(axis="x")
ax.set_axisbelow(True)
ax.set_xlim(pd.to_datetime("2020-03"), pd.to_datetime("2021-05"))
fig.tight_layout()
../_images/9c15f6bfde9ebf0cce5eefa3e5ca972714290100fa5c4ece88c0c80b02f2e097.png

Phase Dependent Timing#

The other method to configure installation phase timing is to define dependent phase completion rates. For instance, instead of providing "TurbineInstallation": "05/01/2020", we could simply wait until 30% of the monopiles are installed by providing ``"TurbineInstallation": ("MonopileInstallation", 0.3)`. Below, we rely on dependencies instead of dates for nearly all phases and show the results. Note, that mixed date and dependency inputs are allowed.

dependent_starts = {
    "MonopileInstallation": "04/01/2020",
    "TurbineInstallation": ("MonopileInstallation", 0.3),
    "ArrayCableInstallation": ("TurbineInstallation", 0.25),
    "OffshoreSubstationInstallation": "08/01/2020",
    "ScourProtectionInstallation": ("ArrayCableInstallation", 0.8),
    "ExportCableInstallation": ("OffshoreSubstationInstallation", 1),
}
config["install_phases"] = dependent_starts
project = ProjectManager(config)
project.run()

df = pd.DataFrame.from_dict(project.phase_dates).T
df.start = pd.to_datetime(df.start)
df.end = pd.to_datetime(df.end)
df.sort_values("start")

fig = plt.figure(figsize=(10, 4))
ax = fig.add_subplot(111)

ax.barh(y=df.index, width=df.end - df.start, left=df.start);

ax.grid(axis="x")
ax.set_axisbelow(True)
ax.set_xlim(pd.to_datetime("2020-03"), pd.to_datetime("2021-05"))
fig.tight_layout()
../_images/f6082dbe87495103c3ce5c86163b74cf7e583a54dcce518fb8560a2dc71ffb8c.png