Skip to content

Commit 1d13d60

Browse files
authored
Patch: Fixed date maintenance timing and runaway processes (#205)
* drop duplicated request ids in process_times counting * update changelog * udpate request summary * ensure default starting date is overridden, not specific * minor port simplifcation, no logic changes * ensure multiple subassemblies can be recreated easily * fix bug in TTP reset logic so there aren't hidden processes * update changelog * bump version
1 parent 3e16b1a commit 1d13d60

11 files changed

Lines changed: 119 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# CHANGELOG
22

3+
## v0.11.2 - 9 July 2025
4+
5+
- Fixes a bug primarily impacting tow-to-port scenarios where individual maintenance and failure
6+
models are not being reset upon either replacement or following a tow-to-port repair under
7+
certain conditions. This allows for these additional processes to be perpetuated throughout the lifecycle
8+
of the simulation while succumbing to the same inital flaw, compounding the number of erroneously
9+
additional events. The issue is resolved by the following:
10+
1. Multiple subassemblies can now be passed to a `Cable` or `System` object during an
11+
interruption, allowing for simpler logic handling.
12+
2. TTP repairs no longer reset the subassemblies at the time of towing to port, and instead reset
13+
the subassemblies after the turbine has been towed to site. Resetting in the final stage
14+
ensures that any newly created processes, and especially fixed date maintenance schedules can
15+
not create extra processes between tow, repair, and site return operations.
16+
- Fixes a bug in the `Metrics.process_times()` calculation inflating the count of total events
17+
when cables are shut down. This is due to immediate logging of the upstream system and cables
18+
shutting down with shared logging data.
19+
- Applies the same fix to the `Metrics.request_summary()` calculation, ensuring duplicated logging
20+
messages are not caught up in the end result.
21+
322
## v0.11.1 - 3 July 2025
423

524
- Fixes a bug causing 25+ hour mooring connection operations to never complete due to shift delays

tests/unit/test_data_classes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,11 @@ def test_SubassemblyData():
619619
N_failure = len(failure_levels)
620620

621621
subassembly = SubassemblyData.from_dict(GENERATOR_SUBASSEMBLY)
622-
maintenance_list = [
623-
Maintenance.from_dict(m) for m in GENERATOR_SUBASSEMBLY["maintenance"]
624-
]
622+
623+
# ensure subassembly level default start date gets passed through
624+
start = {"start_date": GENERATOR_SUBASSEMBLY["maintenance_start"]}
625+
maintenance_list = [start | m for m in GENERATOR_SUBASSEMBLY["maintenance"]]
626+
maintenance_list = [Maintenance.from_dict(m) for m in maintenance_list]
625627
failure_list = [
626628
Failure.from_dict({**f, "rng": RNG}) for f in GENERATOR_SUBASSEMBLY["failures"]
627629
]

wombat/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from wombat.core.library import create_library_structure, load_yaml
55

66

7-
__version__ = "0.11.1"
7+
__version__ = "0.11.2"

wombat/core/data_classes.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,11 @@ def check_start_stop_dates(
490490

491491
def convert_maintenance_list(value: list[dict], self_) -> list[Maintenance]:
492492
"""Converts a list of ``Maintenance`` configuration dictionaries to a list of
493-
``Maintenance`` objects.
493+
``Maintenance`` objects, setting the ``start_date`` if it doesn't exist yet,
494+
and adding the ``self_.system_value``.
494495
"""
495496
kw = {"system_value": self_.system_value, "start_date": self_.maintenance_start}
496-
[el.update(kw) for el in value]
497+
value = [kw | el for el in value]
497498
return [Maintenance.from_dict(el) for el in value]
498499

499500

wombat/core/port.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ def run_tow_to_port(self, request: RepairRequest) -> Generator[Process]:
401401

402402
system_id = request.system_id
403403
request_id = request.request_id
404+
system = self.windfarm.system(system_id)
404405

405406
# Double check in case a delay causes multiple vessels to be interacting with
406407
# the same turbine
@@ -410,15 +411,13 @@ def run_tow_to_port(self, request: RepairRequest) -> Generator[Process]:
410411
self.invalid_systems.append(system_id)
411412

412413
# If the system is already undergoing repairs from other servicing equipment,
413-
# then wait until it's done being serviced
414-
servicing = self.windfarm.system(system_id).servicing
414+
# then wait until it's done being serviced. Also wait for a spot to open up in
415+
# the port queue
415416

416-
# Wait for a spot to open up in the port queue
417417
turbine_request = self.turbine_manager.request()
418-
419-
yield turbine_request & servicing
418+
yield turbine_request & system.servicing
420419
yield self.env.timeout(self.env.get_random_seconds())
421-
yield self.windfarm.system(system_id).servicing
420+
yield system.servicing
422421

423422
# Request a tugboat to retrieve the turbine
424423
tugboat = yield self.service_equipment_manager.get(

wombat/core/post_processor.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,6 +2095,7 @@ def process_times(self, include_incompletes: bool = True) -> pd.DataFrame:
20952095
events_valid.action.isin(("repair request", "maintenance request")),
20962096
["request_id", "env_time"],
20972097
]
2098+
.drop_duplicates(subset=["request_id"])
20982099
.set_index("request_id")
20992100
.sort_index()
21002101
)
@@ -2151,26 +2152,30 @@ def request_summary(self) -> pd.DataFrame:
21512152
requests = self.events.loc[
21522153
self.events.action.isin(("repair request", "maintenance request")),
21532154
"request_id",
2154-
]
2155+
].drop_duplicates()
21552156
canceled_requests = self.events.loc[
21562157
self.events.action.isin(("repair canceled", "maintenance canceled")),
21572158
"request_id",
2158-
]
2159+
].drop_duplicates()
21592160
completed_requests = self.events.loc[
21602161
self.events.action.isin(("repair complete", "maintenance complete")),
21612162
"request_id",
2162-
]
2163+
].drop_duplicates()
21632164
incomplete_requests = requests.loc[
21642165
~requests.isin(canceled_requests) & ~requests.isin(completed_requests)
21652166
]
2166-
total_df = self.events.loc[
2167-
self.events.action.isin(("repair request", "maintenance request")),
2168-
["part_name", "reason", "request_id"],
2169-
].rename(
2170-
columns={
2171-
"part_name": "subassembly",
2172-
"reason": "task",
2173-
}
2167+
total_df = (
2168+
self.events.loc[
2169+
self.events.action.isin(("repair request", "maintenance request")),
2170+
["part_name", "reason", "request_id"],
2171+
]
2172+
.rename(
2173+
columns={
2174+
"part_name": "subassembly",
2175+
"reason": "task",
2176+
}
2177+
)
2178+
.drop_duplicates(subset=["request_id"])
21742179
)
21752180
canceled_df = (
21762181
total_df.loc[total_df.request_id.isin(canceled_requests)]

wombat/core/repair_management.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,27 @@ def invalidate_system(
534534
self.systems_waiting_for_tow.index(system.id)
535535
)
536536

537+
def reset_subassembly_processes(
538+
self, system: System | Cable, subassemblies: list[str]
539+
) -> None:
540+
"""Resets the specified failure and maintenance tasks for each of the
541+
:py:attr:`subassemblies` in the :py:attr:`system`.
542+
543+
Parameters
544+
----------
545+
system : System | Cable
546+
The turbine, substation, electrolyzer, or cable needing to be interrupted
547+
with specified :py:attr:`subassemblies` processes reset.
548+
subassemblies : list[str]
549+
The list of subassemblies to have their failure and maintenance models
550+
recreated (replacement or tow-to-port events only).
551+
"""
552+
system.interrupt_all_subassembly_processes(subassembly_full_reset=subassemblies)
553+
537554
def interrupt_system(
538-
self, system: System | Cable, replacement: str | None = None
555+
self,
556+
system: System | Cable,
557+
subassembly_full_reset: str | list[str] | None = None,
539558
) -> None:
540559
"""Sets the turbine status to be in servicing, and interrupts all the processes
541560
to turn off operations.
@@ -544,13 +563,19 @@ def interrupt_system(
544563
----------
545564
system_id : str
546565
The system to disable repairs.
547-
replacement: str | None, optional
548-
If a subassebly `id` is provided, this indicates the interruption is caused
549-
by its replacement event. Defaults to None.
566+
subassembly_full_reset: str | list[str] | None, optional
567+
If a subassebly `id` is provided, this indicates the interruption will
568+
cancel the current maintenance and failure modes, then recreate the
569+
replacement or tow-to-port repair is completed. Defaults to None.
550570
"""
571+
if subassembly_full_reset is None:
572+
subassembly_full_reset = []
573+
if isinstance(subassembly_full_reset, str):
574+
subassembly_full_reset = [subassembly_full_reset]
575+
551576
if system.servicing.triggered and system.id in self.invalid_systems:
552577
system.servicing = self.env.event()
553-
system.interrupt_all_subassembly_processes(replacement=replacement)
578+
self.reset_subassembly_processes(system, subassembly_full_reset)
554579
else:
555580
raise RuntimeError(
556581
f"{self.env.simulation_time} {system.id} already being serviced"

wombat/core/service_equipment.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,11 @@ def reset_system_operations(system: System, subassembly_resets: list[str]) -> No
144144
The ``subassembly_id`` to reset to good as new, if not assuming all
145145
subassemblies.
146146
"""
147+
system.interrupt_all_subassembly_processes(
148+
subassembly_full_reset=subassembly_resets
149+
)
147150
for subassembly in system.subassemblies:
148-
if subassembly.name in subassembly_resets:
151+
if subassembly.id in subassembly_resets:
149152
subassembly.operating_level = 1.0
150153
subassembly.recreate_processes()
151154

@@ -1494,7 +1497,7 @@ def in_situ_repair(
14941497
replacement = (
14951498
request.subassembly_id if request.details.replacement else None
14961499
)
1497-
self.manager.interrupt_system(system, replacement=replacement)
1500+
self.manager.interrupt_system(system, subassembly_full_reset=replacement)
14981501
yield self.env.process(
14991502
self.crew_transfer(system, subassembly, request, to_system=True)
15001503
)
@@ -1916,8 +1919,7 @@ def run_tow_to_port(self, request: RepairRequest) -> Generator[Process]:
19161919
)
19171920

19181921
# Turn off the turbine
1919-
replacement = request.subassembly_id if request.details.replacement else None
1920-
self.manager.interrupt_system(system, replacement=replacement)
1922+
self.manager.interrupt_system(system)
19211923

19221924
# Unmoor the turbine and tow it back to port
19231925
yield self.env.process(self.mooring_connection(system, request, which="unmoor"))
@@ -1972,7 +1974,6 @@ def run_tow_to_site(
19721974
self.mooring_connection(system, request, which="reconnect")
19731975
)
19741976

1975-
# Reset the turbine back to operating and return to port
19761977
reset_system_operations(system, subassembly_resets)
19771978
self.manager.enable_requests_for_system(system, tow=True)
19781979
yield self.env.process(

wombat/windfarm/system/cable.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,20 +195,20 @@ def recreate_processes(self) -> None:
195195
"""
196196
self.processes = dict(self._create_processes())
197197

198-
def interrupt_processes(self, replacement: str | None = None) -> None:
198+
def interrupt_processes(self, subassembly_full_reset: list[str]) -> None:
199199
"""Interrupts all of the running processes within the subassembly except for the
200200
process associated with failure that triggers the catastrophic failure.
201201
202202
Parameters
203203
----------
204204
subassembly : Subassembly
205205
The subassembly that should have all processes interrupted.
206-
replacement: bool, optional
207-
If a subassebly `id` is provided, this indicates the interruption is caused
208-
by its replacement event. Defaults to None.
206+
subassembly_full_reset: list[str]
207+
List of all subassebly `id` that will get a full reset of their simulated
208+
failure and maintenance processes by a replacement or tow-to-port event.
209209
"""
210210
cause = "failure"
211-
if self.id == replacement:
211+
if self.id in subassembly_full_reset:
212212
cause = "replacement"
213213

214214
for _, process in self.processes.items():
@@ -219,17 +219,21 @@ def interrupt_processes(self, replacement: str | None = None) -> None:
219219
pass
220220

221221
def interrupt_all_subassembly_processes(
222-
self, replacement: str | None = None
222+
self, subassembly_full_reset: list[str] | None = None
223223
) -> None:
224224
"""Thin wrapper for ``interrupt_processes`` for consistent usage with system.
225225
226226
Parameters
227227
----------
228-
replacement: bool, optional
229-
If a subassebly `id` is provided, this indicates the interruption is caused
230-
by its replacement event. Defaults to None.
228+
subassembly_full_reset: list[str] | None, optional
229+
List of all subassebly `id` that will get a full reset of their simulated
230+
failure and maintenance processes by a replacement or tow-to-port event.
231+
Defaults to None.
231232
"""
232-
self.interrupt_processes(replacement=replacement)
233+
subassembly_full_reset = (
234+
[] if subassembly_full_reset is None else subassembly_full_reset
235+
)
236+
self.interrupt_processes(subassembly_full_reset=subassembly_full_reset)
233237

234238
def stop_all_upstream_processes(self, failure: Failure | Maintenance) -> None:
235239
"""Stops all upstream turbines and cables from producing power by creating a

wombat/windfarm/system/subassembly.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ def recreate_processes(self) -> None:
104104
self.processes = dict(self._create_processes())
105105

106106
def interrupt_processes(
107-
self, origin: Subassembly | None = None, replacement: str | None = None
107+
self,
108+
origin: Subassembly | None = None,
109+
subassembly_full_reset: list[str] | None = None,
108110
) -> None:
109111
"""Interrupts all of the running processes within the subassembly except for the
110112
process associated with failure that triggers the catastrophic failure.
@@ -116,14 +118,17 @@ def interrupt_processes(
116118
from a subassembly shutdown event. If provided, and it is the same as the
117119
current subassembly, then a try/except flow is used to ensure the process
118120
that initiated the shutdown is not interrupting itself.
119-
replacement: bool, optional
120-
If a subassebly `id` is provided, this indicates the interruption is caused
121-
by its replacement event. Defaults to None.
121+
subassembly_full_reset: list[str]
122+
List of all subassebly `id` that will get a full reset of their simulated
123+
failure and maintenance processes by a replacement or tow-to-port event.
124+
Defaults to None.
122125
"""
126+
subassembly_full_reset = (
127+
[] if subassembly_full_reset is None else subassembly_full_reset
128+
)
123129
cause = "failure"
124-
if self.id == replacement:
130+
if self.id in subassembly_full_reset:
125131
cause = "replacement"
126-
# if origin is not None and id(origin) == id(self):
127132

128133
# Processes that initiate the process can't be interrupted, nor can replaced
129134
# (already cancelled) processes

0 commit comments

Comments
 (0)