@@ -81,7 +81,156 @@ def __init__(self, name, actions, action_sequence=None, **kwargs):
8181 print (f"---------------------- Initializing Task '{ self .name } ----------------------" )
8282 print (f"Task '{ self .name } ' initialized with duration = { self .duration :.2f} h." )
8383 print (f"Task '{ self .name } ' initialized with cost = ${ self .cost :.2f} " )
84-
84+
85+ def getSequenceGraph (self , action_sequence = None , plot = True ):
86+ '''Generate a multi-directed graph that visalizes action sequencing within the task.
87+ Build a MultiDiGraph with nodes:
88+ Start -> CP1 -> CP2 -> ... -> End
89+
90+ Checkpoints are computed from action "levels":
91+ level(a) = 1 if no prerequisites.
92+ level(a) = 1 + max(level(p) for p in prerequisites) 1 + the largest level among a’s prerequisites.
93+ Number of checkpoints = max(level) - 1.
94+ '''
95+ if action_sequence is None :
96+ action_sequence = self .action_sequence
97+ # Compute levels
98+ levels : dict [str , int ] = {}
99+ def level_of (a : str , b : set [str ]) -> int :
100+ '''Return the level of action a. b is the set of actions currently being explored'''
101+
102+ # If we have already computed the level, return it
103+ if a in levels :
104+ return levels [a ]
105+
106+ if a in b :
107+ raise ValueError (f"Cycle detected in action sequence at '{ a } ' in task '{ self .name } '. The action cannot be its own prerequisite." )
108+
109+ b .add (a )
110+
111+ # Look up prerequisites for action a.
112+ pres = action_sequence .get (a , [])
113+ if not pres :
114+ lv = 1 # No prerequisites, level 1
115+ else :
116+ # If a prerequisites name is not in the dict, treat it as a root (level 1)
117+ lv = 1 + max (level_of (p , b ) if p in action_sequence else 1 for p in pres )
118+ # b.remove(a) # if you want to unmark a from the explored dictionary, b, uncomment this line.
119+ levels [a ] = lv
120+ return lv
121+
122+ for a in action_sequence :
123+ level_of (a , set ())
124+
125+ max_level = max (levels .values (), default = 1 )
126+ num_cps = max (0 , max_level - 1 )
127+
128+ H = nx .MultiDiGraph ()
129+
130+ # Add the Start -> [checkpoints] -> End nodes
131+ H .add_node ("Start" )
132+ for i in range (1 , num_cps + 1 ):
133+ H .add_node (f"CP{ i } " )
134+ H .add_node ("End" )
135+
136+ shells = [["Start" ]]
137+ if num_cps > 0 :
138+ # Middle shells
139+ cps = [f"CP{ i } " for i in range (1 , num_cps + 1 )]
140+ shells .append (cps )
141+ shells .append (["End" ])
142+
143+ pos = nx .shell_layout (H , nlist = shells )
144+
145+ xmin , xmax = - 2.0 , 2.0 # maybe would need to change those later on.
146+ pos ["Start" ] = (xmin , 0 )
147+ pos ["End" ] = (xmax , 0 )
148+
149+ # Add action edges
150+ # Convention:
151+ # level 1 actions: Start -> CP1 (or Start -> End if no CPs)
152+ # level L actions (2 <= L < max_level): CP{L-1} -> CP{L}
153+ # level == max_level actions: CP{num_cps} -> End
154+ for action , lv in levels .items ():
155+ action = self .actions [action ]
156+ if num_cps == 0 :
157+ # No checkpoints: all actions from Start to End
158+ H .add_edge ("Start" , "End" , key = action , duration = action .duration , cost = action .cost )
159+ else :
160+ if lv == 1 :
161+ H .add_edge ("Start" , "CP1" , key = action , duration = action .duration , cost = action .cost )
162+ elif lv < max_level :
163+ H .add_edge (f"CP{ lv - 1 } " , f"CP{ lv } " , key = action , duration = action .duration , cost = action .cost )
164+ else : # lv == max_level
165+ H .add_edge (f"CP{ num_cps } " , "End" , key = action , duration = action .duration , cost = action .cost )
166+ # 3. Compute cumulative start time for each level
167+ level_groups = {}
168+ for action , lv in levels .items ():
169+ level_groups .setdefault (lv , []).append (action )
170+
171+ level_durations = {lv : max (self .actions [a ].duration for a in acts )
172+ for lv , acts in level_groups .items ()}
173+
174+
175+ task_duration = sum (level_durations .values ())
176+ level_start_time = {}
177+ elapsed = 0.0
178+ cp_string = []
179+ for lv in range (1 , max_level + 1 ):
180+ level_start_time [lv ] = elapsed
181+ elapsed += level_durations .get (lv , 0.0 )
182+ # also collect all actions at this level for title
183+ acts = [a for a , l in levels .items () if l == lv ]
184+ if acts and lv <= num_cps :
185+ cp_string .append (f"CP{ lv } : { ', ' .join (acts )} " )
186+ elif acts and lv > num_cps :
187+ cp_string .append (f"End: { ', ' .join (acts )} " )
188+ # Assign to self:
189+ self .sequence_graph = H
190+ title_str = f"Task { self .name } . Duration { self .duration :.2f} : " + " | " .join (cp_string )
191+ if plot :
192+ fig , ax = plt .subplots ()
193+ # pos = nx.shell_layout(G)
194+ nx .draw (H , pos , with_labels = True , node_size = 500 , node_color = "lightblue" , edge_color = 'white' )
195+
196+ label_positions = {} # to store label positions for each edge
197+ # Group edges by unique (u, v) pairs
198+ for (u , v ) in set ((u , v ) for u , v , _ in H .edges (keys = True )):
199+ # get all edges between u and v (dict keyed by edge key)
200+ edge_dict = H .get_edge_data (u , v ) # {key: {attrdict}, ...}
201+ n = len (edge_dict )
202+
203+ # curvature values spread between -0.3 and +0.3 [helpful to visualize multiple edges]
204+ if n == 1 :
205+ rads = [0 ]
206+ offsets = [0.5 ]
207+ else :
208+ rads = np .linspace (- 0.3 , 0.3 , n )
209+ offsets = np .linspace (0.2 , 0.8 , n )
210+
211+ # draw each edge
212+ durations = [d .get ("duration" , 0.0 ) for d in edge_dict .values ()]
213+ scale = max (max (durations ), 0.0001 ) # avoid div by zero
214+ width_scale = 4.0 / scale # normalize largest to ~4px
215+
216+ for rad , offset , (k , d ) in zip (rads , offsets , edge_dict .items ()):
217+ nx .draw_networkx_edges (
218+ H , pos , edgelist = [(u , v )], ax = ax ,
219+ connectionstyle = f"arc3,rad={ rad } " ,
220+ arrows = True , arrowstyle = "-|>" ,
221+ edge_color = "gray" ,
222+ width = max (0.5 , d .get ("duration" , []) * width_scale ),
223+ )
224+ label_positions [(u , v , k )] = offset # store position for edge label
225+
226+ ax .set_title (title_str , fontsize = 12 , fontweight = "bold" )
227+ ax .axis ("off" )
228+ plt .tight_layout ()
229+
230+ return H
231+
232+
233+
85234 def stageActions (self , from_deps = True ):
86235 '''
87236 This method stages the action_sequence for a proper execution order.
0 commit comments