Servicing Strategies#
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.ipynb
locally, 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