2020class Task ():
2121 '''
2222 A Task is a general representation of a set of marine operations
23- that follow a predefined sequency /strategy. There can be multiple
23+ that follow a predefined sequence /strategy. There can be multiple
2424 tasks that achieve the same end, each providing an alternative strategy.
2525 Each Task consists of a set of Actions with internal dependencies.
2626
@@ -60,9 +60,8 @@ def __init__(self, actions, action_sequence, name, **kwargs):
6060
6161
6262 # Create a graph of the sequence of actions in this task based on action_sequence
63-
64- # >>> Rudy to do <<<
65-
63+ self .getSequenceGraph (action_sequence )
64+
6665 self .name = name
6766
6867 self .status = 0 # 0, waiting; 1=running; 2=finished
@@ -104,11 +103,145 @@ def calcDuration(self):
104103
105104
106105
106+ def getSequenceGraph (self , action_sequence ):
107+ '''Generate a multi-directed graph that visalizes action sequencing within the task.
108+ Build a MultiDiGraph with nodes:
109+ Start -> CP1 -> CP2 -> ... -> End
107110
111+ Checkpoints are computed from action "levels":
112+ level(a) = 1 if no prerequisites.
113+ level(a) = 1 + max(level(p) for p in prerequisites) 1 + the largest level among a’s prerequisites.
114+ Number of checkpoints = max(level) - 1.
115+ '''
116+
117+ # Compute levels
118+ levels : dict [str , int ] = {}
119+ def level_of (a : str , b : set [str ]) -> int :
120+ '''Return the level of action a. b is the set of actions currently being explored'''
121+
122+ # If we have already computed the level, return it
123+ if a in levels :
124+ return levels [a ]
125+
126+ # The action cannot be its own prerequisite
127+ if a in b :
128+ raise ValueError (f"Cycle detected in action sequence at '{ a } ' in task '{ self .name } '. The action cannot be its own prerequisite." )
129+
130+ b .add (a )
131+
132+ # Look up prerequisites for action a.
133+ pres = action_sequence .get (a , [])
134+ if not pres :
135+ lv = 1 # No prerequisites, level 1
136+ else :
137+ # If a prerequisites name is not in the dict, treat it as a root (level 1)
138+ lv = 1 + max (level_of (p , b ) if p in action_sequence else 1 for p in pres )
139+
140+ # b.remove(a) # if you want to unmark a from the explored dictionary, b, uncomment this line.
141+ levels [a ] = lv
142+ return lv
143+
144+ for a in action_sequence :
145+ level_of (a , set ())
146+
147+ max_level = max (levels .values (), default = 1 )
148+ num_cps = max (0 , max_level - 1 )
149+
150+ H = nx .MultiDiGraph ()
151+
152+ # Add the Start -> [checkpoints] -> End nodes
153+ H .add_node ("Start" )
154+ for i in range (1 , num_cps + 1 ):
155+ H .add_node (f"CP{ i } " )
156+ H .add_node ("End" )
157+
158+ shells = [["Start" ]]
159+ if num_cps > 0 :
160+ # Middle shells
161+ cps = [f"CP{ i } " for i in range (1 , num_cps + 1 )]
162+ shells .append (cps )
163+ shells .append (["End" ])
164+
165+ pos = nx .shell_layout (H , nlist = shells )
166+
167+ xmin , xmax = - 2.0 , 2.0
168+ pos ["Start" ] = (xmin , 0 )
169+ pos ["End" ] = (xmax , 0 )
170+
171+ # Add action edges
172+ # Convention:
173+ # level 1 actions: Start -> CP1 (or Start -> End if no CPs)
174+ # level L actions (2 <= L < max_level): CP{L-1} -> CP{L}
175+ # level == max_level actions: CP{num_cps} -> End
176+ for action , lv in levels .items ():
177+ action = self .actions [action ]
178+ if num_cps == 0 :
179+ # No checkpoints: all actions from Start to End
180+ H .add_edge ("Start" , "End" , key = action , duration = action .duration , cost = action .cost )
181+ else :
182+ if lv == 1 :
183+ H .add_edge ("Start" , "CP1" , key = action , duration = action .duration , cost = action .cost )
184+ elif lv < max_level :
185+ H .add_edge (f"CP{ lv - 1 } " , f"CP{ lv } " , key = action , duration = action .duration , cost = action .cost )
186+ else : # lv == max_level
187+ H .add_edge (f"CP{ num_cps } " , "End" , key = action , duration = action .duration , cost = action .cost )
188+
189+ fig , ax = plt .subplots ()
190+ # pos = nx.shell_layout(G)
191+ nx .draw (H , pos , with_labels = True , node_size = 500 , node_color = "lightblue" , edge_color = 'white' )
192+
193+ # Group edges by unique (u, v) pairs
194+ for (u , v ) in set ((u , v ) for u , v , _ in H .edges (keys = True )):
195+ # get all edges between u and v (dict keyed by edge key)
196+ edge_dict = H .get_edge_data (u , v ) # {key: {attrdict}, ...}
197+ n = len (edge_dict )
198+
199+ # curvature values spread between -0.3 and +0.3
200+ if n == 1 :
201+ rads = [0 ]
202+ else :
203+ rads = np .linspace (- 0.3 , 0.3 , n )
204+
205+ # draw each edge
206+ durations = [d .get ("duration" , 0.0 ) for d in edge_dict .values ()]
207+ scale = max (max (durations ), 0.0001 )
208+ width_scale = 4.0 / scale # normalize largest to ~4px
209+
210+ for rad , (k , d ) in zip (rads , edge_dict .items ()):
211+ nx .draw_networkx_edges (
212+ H , pos , edgelist = [(u , v )], ax = ax ,
213+ connectionstyle = f"arc3,rad={ rad } " ,
214+ arrows = True , arrowstyle = "-|>" ,
215+ edge_color = "gray" ,
216+ width = max (0.5 , d .get ("duration" , []) * width_scale ),
217+ )
218+
219+ # --- after drawing edges ---
220+ edge_labels = {}
221+ for u , v , k , d in H .edges (keys = True , data = True ):
222+ # each edge may have a unique key; include it in the label if desired
223+ label = k .name
224+ edge_labels [(u , v , k )] = label
225+
226+ nx .draw_networkx_edge_labels (
227+ H ,
228+ pos ,
229+ edge_labels = edge_labels ,
230+ font_size = 8 ,
231+ label_pos = 0.5 , # position along edge (0=start, 0.5=middle, 1=end)
232+ rotate = False # keep labels horizontal
233+ )
234+
235+ ax .axis ("off" )
236+ plt .tight_layout ()
237+ plt .show ()
108238
239+ self .sequence_graph = H
240+ return H
241+
109242
110243
111- def getTaskGraph (self ):
244+ def getTaskGraph (self , plot = True ):
112245 '''Generate a graph of the action dependencies.
113246 '''
114247
@@ -122,7 +255,7 @@ def getTaskGraph(self):
122255 longest_path = nx .dag_longest_path (G , weight = 'duration' )
123256 longest_path_edges = list (zip (longest_path , longest_path [1 :])) # Convert path into edge pairs
124257
125- total_duration = sum (self .actions [node ].duration for node in longest_path )
258+ total_duration = sum (self .actions [node ].duration for node in longest_path )
126259 return G
127260
128261
0 commit comments