Skip to content

Commit

Permalink
Bug Fix: Process recreation and equipment cost breakdowns (#109)
Browse files Browse the repository at this point in the history
* update equipment labor breakdown to include equipment and duration

* add missing process recreation for in situ replacements

* update docstring

* update changelog
  • Loading branch information
RHammond2 authored Sep 5, 2023
1 parent 1be5742 commit 4cfe1ee
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

- `Metrics.equipment_labor_cost_breakdowns` now has a `by_equipment` boolean flag, so that the labor and equipment costs can be broken down by category and equipment. Additionally, `total_hours` has been added to the results, resulting in fewer computed metrics across the same set of breakdowns.
- Subassemblies and cables are now able to resample their next times to failure for all maintenance and failure activities, so that replacement events reset the timing for failures across the board.

## v0.8.1 (28 August 2023)

- Fixes a bug where servicing equipment waiting for the next operational period at the end of a simulation get stuck in an infinite loop because the timeout is set for just prior to the end of the simulation, and not just after the end of the simulation's maximum run time.
Expand Down
51 changes: 41 additions & 10 deletions wombat/core/post_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,11 +1377,15 @@ def labor_costs(
return costs

def equipment_labor_cost_breakdowns(
self, frequency: str, by_category: bool = False
self,
frequency: str,
by_category: bool = False,
by_equipment: bool = False,
) -> pd.DataFrame:
"""Calculates the producitivty cost breakdowns for the simulation at a project,
annual, or monthly level that can be broken out to include the equipment and
labor components.
"""Calculates the producitivty cost and time breakdowns for the simulation at a
project, annual, or monthly level that can be broken out to include the
equipment and labor components, as well as be broken down by servicing
equipment.
.. note:: Doesn't produce a value if there's no cost associated with a "reason".
Expand All @@ -1392,6 +1396,9 @@ def equipment_labor_cost_breakdowns(
by_category : bool, optional
Indicates whether to include the equipment and labor categories (True) or
not (False), by default False.
by_equipment : bool, optional
Indicates whether the values are with resepect to the equipment utilized
(True) or not (False), by default False.
Returns
-------
Expand All @@ -1405,6 +1412,7 @@ def equipment_labor_cost_breakdowns(
- total_labor_cost (if by_category == ``True``)
- equipment_cost (if by_category == ``True``)
- total_cost (if broken out)
- total_hours
Raises
------
Expand All @@ -1416,9 +1424,13 @@ def equipment_labor_cost_breakdowns(
"""
frequency = _check_frequency(frequency, which="all")
if not isinstance(by_category, bool):
raise ValueError("``by_category`` must be one of ``True`` or ``False``")
if not isinstance(by_equipment, bool):
raise ValueError("``by_equipment`` must be one of ``True`` or ``False``")

group_filter = ["action", "reason", "additional"]
if by_equipment:
group_filter.insert(0, "agent")
if frequency in ("annual", "month-year"):
group_filter.insert(0, "year")
elif frequency == "monthly":
Expand All @@ -1433,18 +1445,20 @@ def equipment_labor_cost_breakdowns(
"mobilization",
"transferring crew",
"traveling",
"towing",
]
equipment = self.events[self.events[self._equipment_cost] > 0].agent.unique()
costs = (
self.events.loc[
self.events.agent.isin(equipment)
& self.events.action.isin(action_list)
& ~self.events.additional.isin(["work is complete"]),
group_filter + self._cost_columns,
group_filter + self._cost_columns + ["duration"],
]
.groupby(group_filter)
.sum()
.reset_index()
.rename(columns={"duration": "total_hours"})
)
costs["display_reason"] = [""] * costs.shape[0]

Expand All @@ -1454,8 +1468,15 @@ def equipment_labor_cost_breakdowns(
"no more return visits will be made",
"will return next year",
"waiting for next operational period",
"end of shift; will resume work in the next shift",
)
weather_hours = (
"weather delay",
"weather unsuitable to transfer crew",
"insufficient time to complete travel before end of the shift",
"weather unsuitable for mooring reconnection",
"weather unsuitable for unmooring",
)
weather_hours = ("weather delay", "weather unsuitable to transfer crew")
costs.loc[
(costs.action == "delay") & (costs.additional.isin(non_shift_hours)),
"display_reason",
Expand All @@ -1466,6 +1487,7 @@ def equipment_labor_cost_breakdowns(
costs.action == "transferring crew", "display_reason"
] = "Crew Transfer"
costs.loc[costs.action == "traveling", "display_reason"] = "Site Travel"
costs.loc[costs.action == "towing", "display_reason"] = "Towing"
costs.loc[costs.action == "mobilization", "display_reason"] = "Mobilization"
costs.loc[
costs.additional.isin(weather_hours), "display_reason"
Expand Down Expand Up @@ -1529,20 +1551,29 @@ def equipment_labor_cost_breakdowns(
"Repair",
"Crew Transfer",
"Site Travel",
"Towing",
"Mobilization",
"Weather Delay",
"No Requests",
"Not in Shift",
]
costs.reason = pd.Categorical(costs.reason, new_sort)
costs = costs.set_index(group_filter)
sort_order = ["reason"]
if by_equipment:
costs = costs.loc[costs.index.get_level_values("agent").isin(equipment)]
costs.index = costs.index.set_names({"agent": "equipment_name"})
sort_order = ["equipment_name", "reason"]
if frequency == "project":
return costs.sort_values(by="reason")
return costs.sort_values(by=sort_order)
if frequency == "annual":
return costs.sort_values(by=["year", "reason"])
sort_order = ["year"] + sort_order
return costs.sort_values(by=sort_order)
if frequency == "monthly":
return costs.sort_values(by=["month", "reason"])
return costs.sort_values(by=["year", "month", "reason"])
sort_order = ["month"] + sort_order
return costs.sort_values(by=sort_order)
sort_order = ["year", "month"] + sort_order
return costs.sort_values(by=sort_order)

def emissions(
self,
Expand Down
3 changes: 2 additions & 1 deletion wombat/core/service_equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,13 @@ def register_repair_with_subassembly(
"""
operation_reduction = repair.details.operation_reduction

# Put the subassembly/component back to good as new condition
# Put the subassembly/component back to good as new condition and restart
if repair.details.replacement:
subassembly.operating_level = 1.0
_ = self.manager.purge_subassembly_requests(
repair.system_id, repair.subassembly_id
)
subassembly.recreate_processes()
elif operation_reduction == 1:
subassembly.operating_level = starting_operating_level
subassembly.broken.succeed()
Expand Down
6 changes: 6 additions & 0 deletions wombat/windfarm/system/cable.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ def _create_processes(self):
desc = maintenance.description
yield desc, self.env.process(self.run_single_maintenance(maintenance))

def recreate_processes(self) -> None:
"""If a cable is being reset after a replacement, then all processes are
assumed to be reset to 0, and not pick back up where they left off.
"""
self.processes = dict(self._create_processes())

def interrupt_processes(self) -> None:
"""Interrupts all of the running processes within the subassembly except for the
process associated with failure that triggers the catastrophic failure.
Expand Down
5 changes: 3 additions & 2 deletions wombat/windfarm/system/subassembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ def _create_processes(self):
yield desc, self.env.process(self.run_single_maintenance(maintenance))

def recreate_processes(self) -> None:
"""If a turbine is being reset after a tow-to-port repair, then all processes
are assumed to be reset to 0, and not pick back up where they left off.
"""If a turbine is being reset after a tow-to-port repair or replacement, then
all processes are assumed to be reset to 0, and not pick back up where they left
off.
"""
self.processes = dict(self._create_processes())

Expand Down

0 comments on commit 4cfe1ee

Please sign in to comment.