#!/usr/bin/env python # coding: utf-8 # # NAWEA WOMBAT Interactive Example # # NOTE: This workshop is based on the v0.9 release of WOMBAT, and as of October 26th, this is the current version in the develop branch. # # First, import the necessary packages and make adjust some of the Pandas display settings. # In[1]: from time import perf_counter from pathlib import Path import pandas as pd from wombat import Simulation from wombat.utilities import plot pd.options.display.max_rows = 100 pd.options.display.max_columns = 100 # ## Set up and demonstration of `Simulation` # # First, we create a variable for our library directory, relative to this notebook, and have `Path` resolve it to ensure it exists. Then, we'll create a simulation for both an in situ and tow-to-port simulation (pre-configured in `library/corewind/project/config/`). # # Note that a random seed is being set at the simulation level so that the same results get produced every single time these are run. # In[2]: library_path = Path("../library/corewind/").resolve() sim_in_situ = Simulation(library_path=library_path, config="morro_bay_in_situ.yaml", random_seed=34) sim_ttp = Simulation(library_path=library_path, config="morro_bay_tow_to_port.yaml", random_seed=34) # ### View the farm to verify it's what we saw in the slides # In[3]: plot.plot_farm_layout(sim_in_situ.windfarm, plot_kwargs={"node_size": 200}) # ### Show some of the connections that `Simulation` creates that can be helpful for debugging # # Sometimes simulations fail, and when that happens we might need to dig into what's going, which is often at the service equipment level because that is where much of the strategy configuration is coming from. # # First, we'll see that simulations have a service equipment dictionary controlled by the `name` that we gave them. # In[4]: list(sim_in_situ.service_equipment.keys()) # In[5]: list(sim_ttp.service_equipment.keys()) # Now, let's confirm that our work shifts are set up to be what we discussed. # In[6]: sim_in_situ.service_equipment["Crew Transfer Vessel 1"].settings.workday_start # In[7]: sim_in_situ.service_equipment["Heavy Lift Vessel"].settings.workday_start # ## Run the simulations # # To run a simulation, all we have to do is call `run()` and that will run it until the end of our configured simulation timeframe. Optionally, inputs such as `until=` can be used to control how many simulation hours the simulation will be run (1 year = 8760 hours, but be weary of leap years in your weather timeseries). # In[8]: start = perf_counter() sim_in_situ.run() end = perf_counter() print(f"Run time: {(end - start):.2f} seconds") # In[9]: start = perf_counter() sim_ttp.run() end = perf_counter() print(f"Run time: {(end - start):.2f} seconds") # ## View the results # # Now a `metrics` object will be available through the `Simulation` that gives access to the results methods, which provides all the metrics calculations that are pre-configured within WOMBAT, and gives access to the resulting energy, operations, and events logs. # ### Availability # # Note that the time-based availability is much higher than the production-based availability, which is due to the fact that the time basis cares about if the farm is operational at all, compared to the actual operational levels. If we were to dig into the individual turbines, we would find that the two availabilities would look similar, though the time-based would still trend higher. # In[10]: print("In Situ Availability") print(f'Time-based: {sim_in_situ.metrics.time_based_availability("project", "windfarm").values[0][0]:.2%}') print(f'Production-based: {sim_in_situ.metrics.production_based_availability("project", "windfarm").values[0][0]:.2%}') print() print("Tow-to-Port Availability") print(f'Time-based: {sim_ttp.metrics.time_based_availability("project", "windfarm").values[0][0]:.2%}') print(f'Production-based: {sim_ttp.metrics.production_based_availability("project", "windfarm").values[0][0]:.2%}') # In[11]: plot.plot_farm_availability(sim_in_situ, which="time") # In[12]: plot.plot_farm_availability(sim_in_situ, which="energy") # In[13]: plot.plot_farm_availability(sim_ttp) # In the in situ case, we can see a large dip in availability in January 2003, and in the tow-to-port case we can see a large dip around February 2007, so let's investiage a bit further by diving into the events and operations logs that are available through the `Metrics` object. # In[14]: ev_in_situ = sim_in_situ.metrics.events op_in_situ = sim_in_situ.metrics.operations # In[15]: op_in_situ.head() # In[16]: op_in_situ.loc[op_in_situ.windfarm < 0.45, ["env_datetime", "env_time", "windfarm"]].head(24) # In[17]: ev_in_situ.head().T # transposed because of a bug in Jupyter Lab 4.0 # In[18]: # Get the core subset of columns to show col_filter = ["env_datetime", "agent", "action", "reason", "additional", "system_id", "part_id", "request_id", "duration"] # So let's look for anything that seems like it could cause some major downtime around this timeframe. # In[19]: ev_in_situ.loc[ ev_in_situ.env_datetime >= "2003-01-20", col_filter ].head(50) # So, we have a substation inspection, so in order to operate on that it'll get shut down, which then shuts down all energy passing through it, which could cause some major curtailment. Now, let's see how long that might be lasting # In[20]: ev_in_situ.loc[ ev_in_situ.env_datetime >= "2003-01-20 09:50", col_filter ].head(20) # Ok, so now it's clear that both substations are having simultaneous downtime, which will inevitable shut down the whole farm. Let's just see how long that might last for # In[21]: ss1_maint = ev_in_situ.loc[ ev_in_situ.request_id == "MNT00000325", col_filter ] ss1_maint # So, the request is submitted, and 20 days later it's addressed, so let's figure out roughly how long the downtime actually is here. And then what about the other substation's maintenance? # In[22]: ss1_maint = ss1_maint.loc[ss1_maint.agent == "Crew Transfer Vessel 2"] ss1_maint.env_datetime.max() - ss1_maint.env_datetime.min() # In[23]: ss2_maint = ev_in_situ.loc[ ev_in_situ.request_id == "MNT00000326", col_filter ] ss2_maint.head() # In[24]: ss2_maint = ss2_maint.loc[ss2_maint.agent == "Crew Transfer Vessel 7"] ss2_maint.env_datetime.max() - ss2_maint.env_datetime.min() # So there are 2 simultaneous dips in availability, first from maintenance on the the upstream substation (SS1), and then from maintenance on the main, interconnection-connected substation (SS2), which then causes a shutdown at the entire plant. # # So then what happens for the tow-to-port case? # In[25]: # Exercise for those that want to try it on their own # In[ ]: # ### Cost breakdowns # # Now we can look at the OpEx and see how they compare from one scenario to the next, and break it down by other categories as needed or interested. # In[26]: project_mw = sim_in_situ.windfarm.capacity / 1000 # In[27]: opex = sim_in_situ.metrics.opex("project") opex.index = ["In Situ"] opex.loc["Tow-to-Port", "OpEx"] = sim_ttp.metrics.opex("project").values[0] opex["OpEx (€/MW/yr)"] = opex.OpEx / project_mw / 20 opex = opex.rename(columns={"OpEx": "OpEx (€)"}) opex.style.format("{:,.2f}") # In[28]: opex_breakdown = pd.concat( [ sim_in_situ.metrics.equipment_costs("project"), sim_in_situ.metrics.labor_costs("project"), sim_in_situ.metrics.project_fixed_costs("project", "low"), sim_in_situ.metrics.port_fees("project"), ], axis=1 ) opex_breakdown.index = ["In Situ"] opex_breakdown.loc["Tow-to-Port"] = pd.concat( [ sim_ttp.metrics.equipment_costs("project"), sim_ttp.metrics.labor_costs("project"), sim_ttp.metrics.project_fixed_costs("project", "low"), sim_ttp.metrics.port_fees("project"), ], axis=1 ).values[0] opex_breakdown.style.format("{:,.2f}") # Why might the OpEx for tow-to-port be so low? # In[29]: equipment_breakdown = pd.concat( [ sim_in_situ.metrics.equipment_costs("project", by_equipment=True).T.rename(columns={0: "In Situ"}), sim_ttp.metrics.equipment_costs("project", by_equipment=True).T.rename(columns={0: "Tow-to-Port"}), ], join="outer", axis=1, ).fillna(0) equipment_breakdown.style.format("{:,.2f}") # It should be noted that there are a couple limitations to WOMBAT and this data: # 1. Tugboats don't currently get mobilized, so those costs are missing from the equation # 2. Tugboats are only actively accruing costs during their towing and travel duties, and not during the between time like other servicing equipment # 3. There is no port usage fee, only a monthly access fee, and right now this is just coming directly from ORBIT, so it may not even be the correct amount for an O&M scenario # # In the future, timing TBD, those will be addressed, but for now they're limitations, and should be considered when configuring the tow-to-port costs to match up with expected results # ### Operational performance # # Were the vessels used consistently throughout the simulation period? Note that the below metric probably needs to be updated to be more reflective of vessel inactive time, such as time between shifts, so there are some limitations to the current model # In[30]: equipment_utilization = pd.concat( [ sim_in_situ.metrics.service_equipment_utilization("project").T.rename(columns={0: "In Situ"}), sim_ttp.metrics.service_equipment_utilization("project").T.rename(columns={0: "Tow-to-Port"}), ], join="outer", axis=1, ).fillna(0) equipment_utilization.style.format("{:,.2f}") # What if we also look at the task completion rate? It seems that the tow-to-port scenario isn't actually getting tasks completed, so let's dig deeper. # In[31]: print("In Situ") scheduled = sim_in_situ.metrics.task_completion_rate(which="scheduled", frequency="project").values[0][0] unscheduled = sim_in_situ.metrics.task_completion_rate(which="unscheduled", frequency="project").values[0][0] combined = sim_in_situ.metrics.task_completion_rate(which="both", frequency="project").values[0][0] print(f" Scheduled Task Completion Rate: {scheduled:.2%}") print(f"Unscheduled Task Completion Rate: {unscheduled:.2%}") print(f" Overall Task Completion Rate: {combined:.2%}") print() print("Tow-to-Port") scheduled = sim_ttp.metrics.task_completion_rate(which="scheduled", frequency="project").values[0][0] unscheduled = sim_ttp.metrics.task_completion_rate(which="unscheduled", frequency="project").values[0][0] combined = sim_ttp.metrics.task_completion_rate(which="both", frequency="project").values[0][0] print(f" Scheduled Task Completion Rate: {scheduled:.2%}") print(f"Unscheduled Task Completion Rate: {unscheduled:.2%}") print(f" Overall Task Completion Rate: {combined:.2%}") # So let's dig into how long it takes to start and complete repairs for each scenario # In[32]: process_times = sim_ttp.metrics.process_times() # Normalize the times for hours per failure, to understand the average waiting and repair time time_columns = ["time_to_completion", "process_time", "downtime", "time_to_start"] process_times.loc[:, time_columns] = process_times[time_columns].values / process_times.N.values.reshape(-1, 1) # Sort and make it look nice process_times.sort_values("time_to_completion", ascending=False).style.format("{:,.0f}") # How does that compare to the in situ case for some of our worst offenders? # In[33]: process_times = sim_in_situ.metrics.process_times() # Normalize the times for hours per failure, to understand the average waiting and repair time time_columns = ["time_to_completion", "process_time", "downtime", "time_to_start"] process_times.loc[:, time_columns] = process_times[time_columns].values / process_times.N.values.reshape(-1, 1) # Sort and make it look nice process_times.sort_values("time_to_completion", ascending=False).head(10).style.format("{:,.0f}") # ## What Next? # # - What happens if we add more crew slots at the port to get repairs done? # - What if we add another HLV, does more work get done, or do we just divvy up the tasks more evenly? # - How do labor rates impact the analysis? # - What else do you want to explore? # In[ ]: # NOTE: just remember to delete the logged data when you're done because the CSV files can take up a lot space if you don't need them and are just experimenting # In[34]: sim_in_situ.env.cleanup_log_files() sim_ttp.env.cleanup_log_files() # In[ ]: