Skip to content

Commit 2a95d1b

Browse files
committed
New Gantt-chart creator function for OMAE paper
- new function that takes all the results of the scheduler and writes a new excel-style gantt chart using openpyxl functions - - lots of unique functionality to create a gantt chart that fits for the paper - also adjusted the weather threshold for the AHTS and the weather for the example at the bottom of irma.py, to easily run the OMAE paper case study
1 parent 5d57e79 commit 2a95d1b

File tree

1 file changed

+286
-10
lines changed

1 file changed

+286
-10
lines changed

famodel/irma/irma.py

Lines changed: 286 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,284 @@ def implementStrategy_staged(sc):
637637

638638

639639

640-
640+
def create_gantt_excel(tasks, scheduler_result, weather=None, filename='gantt_chart.xlsx'):
641+
"""
642+
Creates a Gantt chart Excel file from scheduler results.
643+
644+
Parameters:
645+
- tasks: Dictionary of tasks in the Scenario object
646+
- scheduler_result: Result object from the scheduler optimization
647+
- weather: Weather data list (optional, for coloring bad weather periods)
648+
- filename: Output Excel filename
649+
"""
650+
651+
import pandas as pd
652+
from openpyxl import Workbook
653+
from openpyxl.styles import PatternFill, Font, Border, Side, Alignment
654+
from openpyxl.utils import get_column_letter
655+
656+
# Create workbook and worksheet
657+
wb = Workbook()
658+
ws = wb.active
659+
ws.title = "Gantt Chart"
660+
661+
# Define colors
662+
task_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") # Task blue
663+
action_fill = PatternFill(start_color="A9D08E", end_color="A9D08E", fill_type="solid") # Action green
664+
header_fill = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid") # Header light blue
665+
bad_weather_fill = PatternFill(start_color="FF6B6B", end_color="FF6B6B", fill_type="solid") # Light pink for bad weather
666+
667+
# Define fonts
668+
header_font = Font(color="000000", bold=True, size=10) # Black text on light blue
669+
task_font = Font(bold=True, size=11)
670+
action_font = Font(size=10)
671+
cell_font = Font(color="000000", bold=True, size=9) # Black text for cells
672+
673+
# Only border the headers - no borders for data cells
674+
header_border = Border(
675+
left=Side(style='thin'),
676+
right=Side(style='thin'),
677+
top=Side(style='thin'),
678+
bottom=Side(style='thin')
679+
)
680+
681+
# Extract scheduler results
682+
if scheduler_result.get('success', False):
683+
Xta = scheduler_result['Xta'] # Task-Asset assignments
684+
Xtp = scheduler_result['Xtp'] # Task-Period assignments
685+
Xts = scheduler_result['Xts'] # Task-Start assignments
686+
period_duration = scheduler_result.get('period_duration', 0.25)
687+
max_periods = scheduler_result.get('num_periods', 50)
688+
689+
# Create task schedule mapping
690+
task_schedule = {}
691+
for t, task_name in enumerate(scheduler_result['tasks']):
692+
# Find assigned asset group
693+
assigned_asset_group_idx = np.argmax(Xta[t, :]) if Xta[t, :].sum() > 0 else 0
694+
asset_group_info = scheduler_result['asset_groups'][assigned_asset_group_idx]
695+
696+
# Extract asset group name and assets
697+
if isinstance(asset_group_info, dict):
698+
group_name = list(asset_group_info.keys())[0]
699+
asset_list = asset_group_info[group_name]
700+
if isinstance(asset_list, list):
701+
asset_group_display = f"{', '.join(asset_list)}"
702+
else:
703+
asset_group_display = str(group_name)
704+
else:
705+
asset_group_display = f"Group_{assigned_asset_group_idx}"
706+
707+
# Find start time and duration
708+
start_time = np.argmax(Xts[t, :]) if Xts[t, :].sum() > 0 else 0
709+
active_periods = np.where(Xtp[t, :] > 0)[0]
710+
duration = len(active_periods) if len(active_periods) > 0 else 1
711+
712+
task_schedule[task_name] = {
713+
'order': t,
714+
'start_time': int(start_time),
715+
'duration': int(duration),
716+
'asset_group': asset_group_display,
717+
'active_periods': active_periods
718+
}
719+
else:
720+
# Fallback if scheduler failed
721+
max_periods = 50
722+
period_duration = 0.25
723+
task_schedule = {}
724+
for task_name in tasks.keys():
725+
task_schedule[task_name] = {
726+
'order': t,
727+
'start_time': 0,
728+
'duration': 1,
729+
'asset_group': 'Unknown',
730+
'active_periods': [0]
731+
}
732+
733+
# Create headers - Name column + Period columns (no text for periods)
734+
headers = ['Name'] + ['' for i in range(max_periods)]
735+
736+
# Write headers
737+
for col, header in enumerate(headers, 1):
738+
cell = ws.cell(row=1, column=col, value=header)
739+
cell.font = header_font
740+
cell.border = header_border
741+
cell.alignment = Alignment(horizontal='center', vertical='center')
742+
743+
# Apply weather-based coloring to period columns (skip first column which is 'Name')
744+
if col > 1 and weather is not None:
745+
period_index = col - 2 # Convert to 0-based period index
746+
if period_index < len(weather):
747+
if weather[period_index] != 1: # Bad weather
748+
cell.fill = bad_weather_fill
749+
else: # Good weather
750+
cell.fill = header_fill
751+
else:
752+
cell.fill = header_fill
753+
else:
754+
cell.fill = header_fill
755+
756+
# Set row height for header
757+
ws.row_dimensions[1].height = 12
758+
759+
# Set column widths
760+
ws.column_dimensions['A'].width = 25 # Name column (adjusted for Excel display)
761+
for i in range(2, len(headers) + 1): # Period columns
762+
ws.column_dimensions[get_column_letter(i)].width = 1.3
763+
764+
current_row = 2
765+
766+
# Sort tasks by start time if available
767+
sorted_tasks = list(tasks.items())
768+
if task_schedule:
769+
sorted_tasks.sort(key=lambda x: task_schedule.get(x[0], {}).get('order', 0))
770+
771+
# Process each task
772+
for task_name, task in sorted_tasks:
773+
task_info = task_schedule.get(task_name, {})
774+
start_period = task_info.get('start_time', 0)
775+
duration = task_info.get('duration', 1)
776+
asset_group = task_info.get('asset_group', 'Unknown')
777+
active_periods = task_info.get('active_periods', range(start_period, start_period + duration))
778+
779+
# Write task row
780+
task_cell = ws.cell(row=current_row, column=1, value=task_name)
781+
task_cell.font = task_font
782+
task_cell.alignment = Alignment(vertical='center')
783+
784+
# Set row height for task row
785+
ws.row_dimensions[current_row].height = 12
786+
787+
# Task Gantt bars (blue) - only for active periods
788+
if len(active_periods) > 0:
789+
# Create individual cells first
790+
task_cells = []
791+
for period in active_periods:
792+
if period < max_periods:
793+
bar_cell = ws.cell(row=current_row, column=period + 2, value='')
794+
bar_cell.fill = task_fill
795+
task_cells.append((current_row, period + 2))
796+
797+
# Merge cells if there are multiple periods for this task
798+
if len(task_cells) > 1:
799+
start_col = task_cells[0][1]
800+
end_col = task_cells[-1][1]
801+
try:
802+
ws.merge_cells(start_row=current_row, start_column=start_col,
803+
end_row=current_row, end_column=end_col)
804+
# Add asset group name in the merged cell
805+
merged_cell = ws.cell(row=current_row, column=start_col)
806+
merged_cell.value = asset_group
807+
merged_cell.fill = task_fill
808+
merged_cell.font = Font(color="FFFFFF", bold=True, size=11)
809+
merged_cell.alignment = Alignment(horizontal='center', vertical='center')
810+
except:
811+
# If merge fails, just put text in first cell
812+
first_cell = ws.cell(row=current_row, column=start_col)
813+
first_cell.value = asset_group
814+
first_cell.font = Font(color="FFFFFF", bold=True, size=8)
815+
first_cell.alignment = Alignment(horizontal='center', vertical='center')
816+
elif len(task_cells) == 1:
817+
# Single cell - just add the text
818+
single_cell = ws.cell(row=current_row, column=task_cells[0][1])
819+
single_cell.value = asset_group
820+
single_cell.font = Font(color="FFFFFF", bold=True, size=8)
821+
single_cell.alignment = Alignment(horizontal='center', vertical='center')
822+
823+
current_row += 1
824+
825+
# Process actions within this task (with L-shaped indentation)
826+
if hasattr(task, 'actions') and task.actions:
827+
# Sort actions by their timing within the task
828+
sorted_actions = list(task.actions.items())
829+
if hasattr(task, 'actions_ti'):
830+
sorted_actions.sort(key=lambda x: task.actions_ti.get(x[0], 0))
831+
832+
# Calculate action durations and adjust them to fit exactly within task duration
833+
task_duration_periods = len(active_periods)
834+
action_durations = []
835+
total_raw_duration = 0
836+
837+
# First pass: calculate raw durations
838+
for action_name, action in sorted_actions:
839+
if hasattr(action, 'duration'):
840+
raw_duration = max(1, int(action.duration / period_duration))
841+
else:
842+
raw_duration = 1
843+
action_durations.append(raw_duration)
844+
total_raw_duration += raw_duration
845+
846+
# Adjust durations to fit exactly within task duration
847+
if total_raw_duration != task_duration_periods and total_raw_duration > 0:
848+
# Proportionally scale each action duration
849+
adjusted_durations = []
850+
cumulative_adjusted = 0
851+
852+
for i, raw_duration in enumerate(action_durations):
853+
if i == len(action_durations) - 1:
854+
# Last action gets remaining periods to ensure exact fit
855+
adjusted_duration = task_duration_periods - cumulative_adjusted
856+
else:
857+
# Proportional scaling for other actions
858+
adjusted_duration = max(1, int(raw_duration * task_duration_periods / total_raw_duration))
859+
860+
adjusted_durations.append(max(1, adjusted_duration)) # Ensure minimum 1 period
861+
cumulative_adjusted += adjusted_durations[-1]
862+
863+
action_durations = adjusted_durations
864+
865+
# Second pass: create action rows with adjusted durations
866+
action_start_period = start_period
867+
for i, (action_name, action) in enumerate(sorted_actions):
868+
# Create L-shaped symbol for hierarchy - all actions get L-shape
869+
indent_symbol = "└─ " # All actions get the L-shape
870+
871+
action_display_name = f"{indent_symbol}{action_name}"
872+
873+
# Write action row
874+
action_cell = ws.cell(row=current_row, column=1, value=action_display_name)
875+
action_cell.font = action_font
876+
action_cell.alignment = Alignment(vertical='center')
877+
878+
# Set row height for action row
879+
ws.row_dimensions[current_row].height = 12
880+
881+
# Use adjusted duration
882+
action_duration = action_durations[i] if i < len(action_durations) else 1
883+
884+
# Action Gantt bars (green) - sequential within task periods
885+
action_end = min(action_start_period + action_duration, start_period + task_duration_periods)
886+
for period in range(action_start_period, action_end):
887+
if period < max_periods and period in active_periods:
888+
bar_cell = ws.cell(row=current_row, column=period + 2, value='')
889+
bar_cell.fill = action_fill
890+
891+
# Next action starts where this one ended
892+
action_start_period = action_end
893+
current_row += 1
894+
895+
# Freeze panes to keep headers and name column visible
896+
ws.freeze_panes = ws['B2']
897+
898+
# Set zoom level to 150%
899+
ws.sheet_view.zoomScale = 150
900+
901+
# Set print settings for fitting on page
902+
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
903+
ws.page_setup.paperSize = ws.PAPERSIZE_LETTER
904+
ws.page_setup.fitToWidth = 1
905+
ws.page_setup.fitToHeight = 0 # Allow multiple pages vertically if needed
906+
907+
# Set margins to minimum
908+
ws.page_margins.left = 0.25
909+
ws.page_margins.right = 0.25
910+
ws.page_margins.top = 0.5
911+
ws.page_margins.bottom = 0.5
912+
913+
# Save the workbook
914+
wb.save(filename)
915+
print(f"Gantt chart saved to {filename}")
916+
917+
return wb
641918

642919

643920

@@ -815,7 +1092,7 @@ def implementStrategy_staged(sc):
8151092
for asset in sc.vessels.values():
8161093
if 'name' in asset:
8171094
if asset['name'] == 'AHTS_alpha':
818-
asset['max_weather'] = 3
1095+
asset['max_weather'] = 2
8191096
elif asset['name'] == 'MPSV_01':
8201097
asset['max_weather'] = 1
8211098
else:
@@ -974,11 +1251,11 @@ def implementStrategy_staged(sc):
9741251

9751252

9761253
weather = [int(x) for x in np.ones(num_periods, dtype=int)]
977-
'''
978-
weather = [ 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
979-
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
980-
1, 1, 2, 2]
981-
'''
1254+
for i in range(6):
1255+
weather[i] = 2
1256+
for i in range(35, 50):
1257+
weather[i] = 2
1258+
9821259

9831260
scheduler = Scheduler(
9841261
tasks=tasks_scheduler,
@@ -997,7 +1274,6 @@ def implementStrategy_staged(sc):
9971274

9981275
result = scheduler.optimize()
9991276

1000-
a = 2
1001-
1277+
wb = create_gantt_excel(sc.tasks, result, weather, 'installation_gantt_chart.xlsx')
10021278

1003-
1279+
a = 2

0 commit comments

Comments
 (0)