Servicing Strategies#

Binder

In this example, we'll demonstrate the essential differences in scheduled servicing, unscheduled servicing, and tow-to-port repair strategies. Each of the examples demonstrated below will be based on the 2015 Dinwoodie, et al. paper, though the variations will be for demonstration purposes only.

A Jupyter notebook of this tutorial can be run from examples/strategy_demonstration.ipynblocally, or through binder.

WOMBAT Setup and Variables#

The vessels that will be changed in this demonstration are the field support vessel (FSV) with capability: "SCN", the heavy lift vessel (HLV) with capability: "LCN", and the tugboats, which have capability: "TOW".

Note

When running tow-to-port a Port configuration is also required, which will control the tugboats. However, the port costs will still be accounted for in the FixedCosts class as port fees are assumed to be constant. These costs are not considered in this example, so differences in cost should be taken with a grain of salt given the reduction of HLV and FSV operational and mobilization costs.

Scenario descriptions:

  • Scheduled: exactly the same as the base case (fsv_scheduled.yaml and hlv_scheduled.yaml)

  • Unscheduled: requests: the FSV and HLV are called to site when 10 requests that they can service are logged (fsv_requests.yaml and hlv_requests.yaml)

  • Unscheduled: downtime: the FSV and HLV are called to site once the wind farm's operating level hits 90% or lower (fsv_downtime.yaml and hlv_downtime.yaml)

  • Unscheduled: tow-to-port: the FSV and HLV will be replaced with three identical tugboats (tugboat1.yaml, tugboat2.yaml, tugboat3.yaml), and all the failures associated with the FSV and HLV will be changed to capability "TOW" to trigger the tow-to-port repairs. These processes will be triggered on the first request (WOMBAT base assumption that can't be changed for now).

In this example, we will demonstrate how the results for the base case for the Dinwoodie, et al. example vary based on how each of the vessels are scheduled. The configuration details all remain the same, regardless of details, except for the strategy information, which is defined as follows:

This example is set up similarly to that of the validation cases to show how the results differ, and not a step-by-step guide for setting up the analyses. We refer the reader to the extensive documentation and How To example for more information.

Imports and notebook configuration#

from copy import deepcopy
from time import perf_counter

import pandas as pd

from wombat.core import Simulation
from wombat.core.library import DINWOODIE

pd.set_option("display.max_rows", 1000)
pd.set_option("display.max_columns", 1000)
pd.options.display.float_format = '{:,.2f}'.format

Simulation and results setup#

Here we're providing the names of the configuration files (found at: dinwoodie / config) without their .yaml extensions (added in later) and the results that we want to compare between simulations to understand some of the timing and cost trade-offs between simulations.

The dictionary of keys and lists will be used to create the results data frame where the keys will be the indices and the lists will be the row values for each of the above configurations.

configs = [
    "base_scheduled",
    "base_requests",
    "base_downtime",
    "base_tow_to_port",
]

columns = deepcopy(configs) # Create a unique copy of the config names for column naming
results = {
    "availability - time based": [],
    "availability - production based": [],
    "capacity factor - net": [],
    "capacity factor - gross": [],
    "power production": [],
    "task completion rate": [],
    "annual direct O&M cost": [],
    "annual vessel cost": [],
    "ctv cost": [],
    "fsv cost": [],
    "hlv cost": [],
    "tow cost": [],
    "annual repair cost": [],
    "annual technician cost": [],
    "ctv utilization": [],
    "fsv utilization": [],
    "hlv utilization": [],
    "tow utilization": [],
}

Run the simulations and display the results#

timing_df = pd.DataFrame([], columns=["Load Time (min)", "Run Time (min)"], index=configs)
timing_df.index.name = "Scenario"

for config in configs:

    # Load the simulation
    start = perf_counter()
    sim = Simulation(DINWOODIE , f"{config}.yaml")
    end = perf_counter()
    timing_df.loc[config, "Load Time (min)"] = (end - start) / 60

    # Run the simulation
    start = perf_counter()
    sim.run()
    end = perf_counter()
    timing_df.loc[config, "Run Time (min)"] = (end - start) / 60

    # Gather the results of interest
    years = sim.metrics.events.year.unique().shape[0]
    mil = 1000000

    # Gather the high-level results for the simulation
    availability_time = sim.metrics.time_based_availability(frequency="project", by="windfarm")
    availability_production = sim.metrics.production_based_availability(frequency="project", by="windfarm")
    cf_net = sim.metrics.capacity_factor(which="net", frequency="project", by="windfarm")
    cf_gross = sim.metrics.capacity_factor(which="gross", frequency="project", by="windfarm")
    power_production = sim.metrics.power_production(frequency="project", by="windfarm")
    completion_rate = sim.metrics.task_completion_rate(which="both", frequency="project")
    parts = sim.metrics.events[["materials_cost"]].sum().sum()
    techs = sim.metrics.project_fixed_costs(frequency="project", resolution="low").operations[0]
    total = sim.metrics.events[["total_cost"]].sum().sum()

    # Gather the equipment costs and separate the results by equipment type
    equipment = sim.metrics.equipment_costs(frequency="project", by_equipment=True)
    equipment_sum = equipment.sum().sum()
    hlv = equipment[[el for el in equipment.columns if "Heavy Lift Vessel" in el]].sum().sum()
    fsv = equipment[[el for el in equipment.columns if "Field Support Vessel" in el]].sum().sum()
    ctv = equipment[[el for el in equipment.columns if "Crew Transfer Vessel" in el]].sum().sum()
    tow = equipment[[el for el in equipment.columns if "Tugboat" in el]].sum().sum()

    # Gather the equipment utilization data frame and separate the results by equipment type
    utilization = sim.metrics.service_equipment_utilization(frequency="project")
    hlv_ur = utilization[[el for el in utilization.columns if "Heavy Lift Vessel" in el]].mean().mean()
    fsv_ur = utilization[[el for el in utilization.columns if "Field Support Vessel" in el]].mean().mean()
    ctv_ur = utilization[[el for el in utilization.columns if "Crew Transfer Vessel" in el]].mean().mean()
    tow_ur = utilization[[el for el in utilization.columns if "Tugboat" in el]].mean().mean()

    # Log the results of interest
    results["availability - time based"].append(availability_time.values[0][0])
    results["availability - production based"].append(availability_production.values[0][0])
    results["capacity factor - net"].append(cf_net.values[0][0])
    results["capacity factor - gross"].append(cf_gross.values[0][0])
    results["power production"].append(power_production.values[0][0])
    results["task completion rate"].append(completion_rate.values[0][0])
    results["annual direct O&M cost"].append((total + techs) / mil / years)
    results["annual vessel cost"].append(equipment_sum / mil / years)
    results["ctv cost"].append(ctv / mil / years)
    results["fsv cost"].append(fsv / mil / years)
    results["hlv cost"].append(hlv / mil / years)
    results["tow cost"].append(tow / mil / years)
    results["annual repair cost"].append(parts / mil / years)
    results["annual technician cost"].append(techs / mil / years)
    results["ctv utilization"].append(ctv_ur)
    results["fsv utilization"].append(fsv_ur)
    results["hlv utilization"].append(hlv_ur)
    results["tow utilization"].append(tow_ur)

    # Clear the logs
    sim.env.cleanup_log_files()

timing_df
Simulation failed at hour 3,089.749722, simulation time: 2003-05-09 17:45:00
/opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/environment.py:507: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead.
  .resample("H")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1600, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1599 # Register the repair
-> 1600 self.register_repair_with_subassembly(
   1601     subassembly, request, starting_operational_level
   1602 )
   1603 action = "maintenance" if isinstance(request.details, Maintenance) else "repair"

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:492, in ServiceEquipment.register_repair_with_subassembly(self, subassembly, repair, starting_operating_level)
    489     _ = self.manager.purge_subassembly_requests(
    490         repair.system_id, repair.subassembly_id
    491     )
--> 492     subassembly.recreate_processes()
    493 elif operation_reduction == 1:

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/windfarm/system/subassembly.py:89, in Subassembly.recreate_processes(self)
     85 """If a turbine is being reset after a tow-to-port repair or replacement, then
     86 all processes are assumed to be reset to 0, and not pick back up where they left
     87 off.
     88 """
---> 89 self.processes = dict(self._create_processes())

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/windfarm/system/subassembly.py:71, in Subassembly._create_processes(self)
     70 for maintenance in self.data.maintenance:
---> 71     maintenance._update_event_timing(
     72         self.env.start_datetime, self.env.end_datetime, self.env.max_run_time
     73     )
     75 for failure in self.data.failures:

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/data_classes.py:572, in Maintenance._update_event_timing(self, start, end, max_run_time)
    571 if not date_based:
--> 572     diff = relativedelta(**{self.frequency_basis: self.frequency})  # type: ignore
    573     object.__setattr__(self, "frequency", diff)

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/dateutil/relativedelta.py:179, in relativedelta.__init__(self, dt1, dt2, years, months, days, leapdays, weeks, hours, minutes, seconds, microseconds, year, month, day, weekday, yearday, nlyearday, hour, minute, second, microsecond)
    178 self.months = int(months)
--> 179 self.days = days + weeks * 7
    180 self.leapdays = leapdays

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1590, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1588 yield self.env.process(self.wait_until_next_shift(**shared_logging))
-> 1590 yield self.env.process(
   1591     self.in_situ_repair(
   1592         request,
   1593         time_processed=hours_processed + time_processed,
   1594         prior_operation_level=starting_operational_level,
   1595     )
   1596 )
   1597 return

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1590, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1588 yield self.env.process(self.wait_until_next_shift(**shared_logging))
-> 1590 yield self.env.process(
   1591     self.in_situ_repair(
   1592         request,
   1593         time_processed=hours_processed + time_processed,
   1594         prior_operation_level=starting_operational_level,
   1595     )
   1596 )
   1597 return

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1590, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1588 yield self.env.process(self.wait_until_next_shift(**shared_logging))
-> 1590 yield self.env.process(
   1591     self.in_situ_repair(
   1592         request,
   1593         time_processed=hours_processed + time_processed,
   1594         prior_operation_level=starting_operational_level,
   1595     )
   1596 )
   1597 return

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1590, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1588 yield self.env.process(self.wait_until_next_shift(**shared_logging))
-> 1590 yield self.env.process(
   1591     self.in_situ_repair(
   1592         request,
   1593         time_processed=hours_processed + time_processed,
   1594         prior_operation_level=starting_operational_level,
   1595     )
   1596 )
   1597 return

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1590, in ServiceEquipment.in_situ_repair(self, request, time_processed, prior_operation_level, initial)
   1588 yield self.env.process(self.wait_until_next_shift(**shared_logging))
-> 1590 yield self.env.process(
   1591     self.in_situ_repair(
   1592         request,
   1593         time_processed=hours_processed + time_processed,
   1594         prior_operation_level=starting_operational_level,
   1595     )
   1596 )
   1597 return

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/service_equipment.py:1701, in ServiceEquipment.run_scheduled_in_situ(self)
   1700 yield system.servicing
-> 1701 yield self.env.process(self.in_situ_repair(request, initial=True))

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[3], line 14
     12 # Run the simulation
     13 start = perf_counter()
---> 14 sim.run()
     15 end = perf_counter()
     16 timing_df.loc[config, "Run Time (min)"] = (end - start) / 60

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/simulation_api.py:439, in Simulation.run(self, until, create_metrics, save_metrics_inputs)
    416 def run(
    417     self,
    418     until: int | float | Event | None = None,
    419     create_metrics: bool = True,
    420     save_metrics_inputs: bool = True,
    421 ):
    422     """Calls ``WombatEnvironment.run()`` and gathers the results for
    423     post-processing. See ``wombat.simulation.WombatEnvironment.run`` or
    424     ``simpy.Environment.run`` for more details.
   (...)    437         False, the data will not be saved, by default True.
    438     """
--> 439     self.env.run(until=until)
    440     if save_metrics_inputs:
    441         self.save_metrics_inputs()

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/environment.py:251, in WombatEnvironment.run(self, until)
    246     self._operations_csv.close()
    247     print(
    248         f"Simulation failed at hour {self.now:,.6f},"
    249         f" simulation time: {self.simulation_time}"
    250     )
--> 251     raise e
    253 # Ensure all logged events make it to their target file
    254 self._events_writer.writerows(self._events_buffer)

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/wombat/core/environment.py:238, in WombatEnvironment.run(self, until)
    236     until = self.max_run_time
    237 try:
--> 238     super().run(until=until)
    239 except BaseException as e:
    240     # Flush the logs to so the simulation up to the point of failure is logged
    241     self._events_writer.writerows(self._events_buffer)

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/simpy/core.py:246, in Environment.run(self, until)
    244 try:
    245     while True:
--> 246         self.step()
    247 except StopSimulation as exc:
    248     return exc.args[0]  # == until.value

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/simpy/core.py:204, in Environment.step(self)
    202 exc = type(event._value)(*event._value.args)
    203 exc.__cause__ = event._value
--> 204 raise exc

TypeError: unsupported operand type(s) for +: 'relativedelta' and 'int'
results_df = pd.DataFrame(results.values(), columns=columns, index=results.keys()).fillna(0)
results_df
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/internals/construction.py:939, in _finalize_columns_and_data(content, columns, dtype)
    938 try:
--> 939     columns = _validate_or_indexify_columns(contents, columns)
    940 except AssertionError as err:
    941     # GH#26429 do not raise user-facing AssertionError

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/internals/construction.py:986, in _validate_or_indexify_columns(content, columns)
    984 if not is_mi_list and len(columns) != len(content):  # pragma: no cover
    985     # caller's responsibility to check for this...
--> 986     raise AssertionError(
    987         f"{len(columns)} columns passed, passed data had "
    988         f"{len(content)} columns"
    989     )
    990 if is_mi_list:
    991     # check if nested list column, length of each sub-list should be equal

AssertionError: 4 columns passed, passed data had 0 columns

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 results_df = pd.DataFrame(results.values(), columns=columns, index=results.keys()).fillna(0)
      2 results_df

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/frame.py:851, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    849     if columns is not None:
    850         columns = ensure_index(columns)
--> 851     arrays, columns, index = nested_data_to_arrays(
    852         # error: Argument 3 to "nested_data_to_arrays" has incompatible
    853         # type "Optional[Collection[Any]]"; expected "Optional[Index]"
    854         data,
    855         columns,
    856         index,  # type: ignore[arg-type]
    857         dtype,
    858     )
    859     mgr = arrays_to_mgr(
    860         arrays,
    861         columns,
   (...)    864         typ=manager,
    865     )
    866 else:

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/internals/construction.py:520, in nested_data_to_arrays(data, columns, index, dtype)
    517 if is_named_tuple(data[0]) and columns is None:
    518     columns = ensure_index(data[0]._fields)
--> 520 arrays, columns = to_arrays(data, columns, dtype=dtype)
    521 columns = ensure_index(columns)
    523 if index is None:

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/internals/construction.py:845, in to_arrays(data, columns, dtype)
    842     data = [tuple(x) for x in data]
    843     arr = _list_to_arrays(data)
--> 845 content, columns = _finalize_columns_and_data(arr, columns, dtype)
    846 return content, columns

File /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/pandas/core/internals/construction.py:942, in _finalize_columns_and_data(content, columns, dtype)
    939     columns = _validate_or_indexify_columns(contents, columns)
    940 except AssertionError as err:
    941     # GH#26429 do not raise user-facing AssertionError
--> 942     raise ValueError(err) from err
    944 if len(contents) and contents[0].dtype == np.object_:
    945     contents = convert_object_array(contents, dtype=dtype)

ValueError: 4 columns passed, passed data had 0 columns