@@ -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