Skip to content

Commit 118cd02

Browse files
committed
adding a sequence graph to the Task class
1 parent be1ea51 commit 118cd02

File tree

2 files changed

+143
-7
lines changed

2 files changed

+143
-7
lines changed

famodel/irma/irma.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,10 @@ def implementStrategy_staged(sc):
450450
act_sequence[acts[i].name] = []
451451
else: # remaining actions are just a linear sequence
452452
act_sequence[acts[i].name] = [ acts[i-1].name ] # (previous action must be done first)
453-
453+
# Just for testing different examples.
454+
# act_sequence = {'install_anchor-fowt0a': [],
455+
# 'install_anchor-fowt0b': ['install_anchor-fowt0a'],
456+
# 'install_anchor-fowt0c': ['install_anchor-fowt0a']}
454457
sc.addTask(acts, act_sequence, 'install_all_anchors')
455458

456459
# ----- Create a Task for all the mooring installs -----

famodel/irma/task.py

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class 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

Comments
 (0)