Skip to content

Commit 16b96a1

Browse files
committed
Working on new Task-related methods in Scenario:
- Added Scenario methods to add and register Tasks. - Added a action_sequence input in Task that provides the ordering of actions within the task, including parallel/serial bits. - Started an example "strategy implementer" function in irma that sets up one Task for anchor installation to start with. - Added ti/tf placeholders in Action and Task for future timing. - All work in progress...
1 parent ff60470 commit 16b96a1

File tree

3 files changed

+175
-15
lines changed

3 files changed

+175
-15
lines changed

famodel/irma/action.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def __init__(self, actionType, name, **kwargs):
126126

127127
self.duration = getFromDict(actionType, 'duration', default=0) # this will be overwritten by calcDurationAndCost. TODO: or should it overwrite any duration calculation?
128128
self.cost = 0 # this will be overwritten by calcDurationAndCost
129+
self.ti = 0 # action start time [h?]
130+
self.tf = 0 # action end time [h?]
129131

130132
self.supported_objects = [] # list of FAModel object types supported by the action
131133

famodel/irma/irma.py

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def __init__(self):
237237

238238
# Initialize some things
239239
self.actions = {}
240+
self.tasks = {}
240241

241242

242243
def registerAction(self, action):
@@ -331,7 +332,35 @@ def visualizeActions(self):
331332
i += 1
332333
plt.legend()
333334
return G
335+
336+
337+
def registerTask(self, task):
338+
'''Registers an already created task'''
339+
340+
# this also handles creation of unique dictionary keys
341+
342+
if task.name in self.tasks: # check if there is already a key with the same name
343+
raise Warning(f"Action '{task.name}' is already registered.")
344+
print(f"Task name '{task.name}' is in the tasks list so incrementing it...")
345+
task.name = increment_name(task.name)
346+
347+
# Add it to the actions dictionary
348+
self.tasks[task.name] = task
349+
350+
351+
def addTask(self, actions, action_sequence, task_name, **kwargs):
352+
'''Creates a task and adds it to the register'''
353+
354+
# Create the action
355+
task = Task(actions, action_sequence, task_name, **kwargs)
356+
357+
# Register the action
358+
self.registerTask(task)
334359

360+
return task
361+
362+
363+
335364
def findCompatibleVessels(self):
336365
'''Go through actions and identify which vessels have the required
337366
capabilities (could be based on capability presence, or quantitative.
@@ -340,6 +369,101 @@ def findCompatibleVessels(self):
340369
pass
341370

342371

372+
def figureOutTaskRelationships(self):
373+
'''Calculate timing within tasks and then figure out constraints
374+
between tasks.
375+
'''
376+
377+
# Figure out task durations (for a given set of asset assignments?)
378+
for task in self.tasks.values():
379+
task.calcTiming()
380+
381+
# Figure out timing constraints between tasks based on action dependencies
382+
n = len(self.tasks)
383+
dt_min = np.zeros((n,n)) # matrix of required time offsets between tasks
384+
385+
for i1, task1 in enumerate(self.tasks.values()):
386+
for i2, task2 in enumerate(self.tasks.values()):
387+
# look at all action dependencies from tasks 1 to 2 and
388+
# identify the limiting case (the largest time offset)...
389+
dt_min_1_2, dt_min_2_1 = findTaskDependencies(task1, task2)
390+
391+
# for now, just look in one direction
392+
dt_min[i1, i2] = dt_min_1_2
393+
394+
return dt_min
395+
396+
397+
def findTaskDependencies(task1, task2):
398+
'''Finds any time dependency between the actions of two tasks.
399+
Returns the minimum time separation required from task 1 to task 2,
400+
and from task 2 to task 1. I
401+
'''
402+
403+
time_1_to_2 = []
404+
time_2_to_1 = []
405+
406+
# Look for any dependencies where act2 depends on act1:
407+
#for i1, act1 in enumerate(task1.actions.values()):
408+
# for i2, act2 in enumerate(task2.actions.values()):
409+
for a1, act1 in task1.actions.items():
410+
for a2, act2 in task2.actions.items():
411+
412+
if a1 in act2.dependencies: # if act2 depends on act1
413+
time_1_to_2.append(task1.actions_ti[a1] + act1.duration
414+
- task2.actions_ti[a2])
415+
416+
if a2 in act1.dependencies: # if act2 depends on act1
417+
time_2_to_1.append(task2.actions_ti[a2] + act2.duration
418+
- task1.actions_ti[a1])
419+
420+
print(time_1_to_2)
421+
print(time_2_to_1)
422+
423+
dt_min_1_2 = min(time_1_to_2) # minimum time required from t1 start to t2 start
424+
dt_min_2_1 = min(time_2_to_1) # minimum time required from t2 start to t1 start
425+
426+
if dt_min_1_2 + dt_min_2_1 > 0:
427+
print(f"The timing between these two tasks seems to be impossible...")
428+
429+
breakpoint()
430+
return dt_min_1_2, dt_min_2_1
431+
432+
433+
def implementStrategy_staged(sc):
434+
'''This sets up Tasks in a way that implements a staged installation
435+
strategy where all of one thing is done before all of a next thing.
436+
'''
437+
438+
# ----- Create a Task for all the anchor installs -----
439+
440+
# gather the relevant actions
441+
acts = []
442+
for action in sc.actions.values():
443+
if action.type == 'install_anchor':
444+
acts.append(action)
445+
446+
# create a dictionary of dependencies indicating that these actions are all in series
447+
act_sequence = {} # key is action name, value is a list of what action names are to be completed before it
448+
for i in range(len(acts)):
449+
if i==0: # first action has no dependencies
450+
act_sequence[acts[i].name] = []
451+
else: # remaining actions are just a linear sequence
452+
act_sequence[acts[i].name] = [ acts[i-1].name ] # (previous action must be done first)
453+
454+
sc.addTask(acts, act_sequence, 'install_all_anchors')
455+
456+
# ----- Create a Task for all the mooring installs -----
457+
458+
459+
460+
461+
# ----- Create a Task for the platform tow-out and hookup -----
462+
463+
464+
465+
466+
343467
if __name__ == '__main__':
344468
'''This is currently a script to explore how some of the workflow could
345469
work. Can move things into functions/methods as they solidify.
@@ -436,17 +560,18 @@ def findCompatibleVessels(self):
436560

437561

438562
# ----- Generate tasks (groups of Actions according to specific strategies) -----
439-
tasks = []
440-
t1 = Task(sc.actions, 'install_mooring_system')
441-
tasks.append(t1)
563+
564+
#t1 = Task(sc.actions, 'install_mooring_system')
442565

443566
# ----- Do some graph analysis -----
444567

445568
G = sc.visualizeActions()
446569

570+
# make some tasks with one strategy...
571+
implementStrategy_staged(sc)
447572

448573

449-
574+
# dt_min = sc.figureOutTaskRelationships()
450575

451576

452577

@@ -460,8 +585,8 @@ def findCompatibleVessels(self):
460585

461586
# ----- Generate the task_asset_matrix for scheduler -----
462587
# UNUSED FOR NOW
463-
task_asset_matrix = np.zeros((len(tasks), len(sc.vessels), 2))
464-
for i, task in enumerate(tasks):
588+
task_asset_matrix = np.zeros((len(sc.tasks), len(sc.vessels), 2))
589+
for i, task in enumerate(sc.tasks.values()):
465590
row = task.get_row(sc.vessels)
466591
if row.shape != (len(sc.vessels), 2):
467592
raise Exception(f"Task '{task.name}' get_row output has wrong shape {row.shape}, should be {(2, len(sc.vessels))}")

famodel/irma/task.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ class Task():
3232
3333
'''
3434

35-
def __init__(self, actionList, name, **kwargs):
35+
def __init__(self, actions, action_sequence, name, **kwargs):
3636
'''Create an action object...
3737
It must be given a name and a list of actions.
3838
The action list should be by default coherent with actionTypes dictionary.
3939
4040
Parameters
4141
----------
42-
actionList : list
42+
actions : list
4343
A list of all actions that are part of this task.
44+
action_sequence : dict
45+
A dictionary where each key is the name of each action, and the values are
46+
each a list of which actions (by name) must be completed before the current
47+
one.
4448
name : string
4549
A name for the action. It may be appended with numbers if there
4650
are duplicate names.
@@ -49,18 +53,35 @@ def __init__(self, actionList, name, **kwargs):
4953
5054
'''
5155

52-
self.actionList = actionList # all actions that are carried out in this task
56+
# Make a dict by name of all actions that are carried out in this task
57+
self.actions = {}
58+
for act in actions:
59+
self.actions[act.name] = act
60+
61+
62+
# Create a graph of the sequence of actions in this task based on action_sequence
63+
64+
# >>> Rudy to do <<<
65+
5366
self.name = name
67+
5468
self.status = 0 # 0, waiting; 1=running; 2=finished
5569

70+
self.actions_ti = {} # relative start time of each action [h]
71+
self.t_actions = {} # timing of task's actions, relative to t1 [h]
72+
# t_actions is a dict with keys same as action names, and entries of [t1, t2]
73+
5674
self.duration = 0 # duration must be calculated based on lengths of actions
5775
self.cost = 0 # cost must be calculated based on the cost of individual actions.
58-
76+
self.ti =0 # task start time [h?]
77+
self.tf =0 # task end time [h?]
78+
5979
# what else do we need to initialize the task?
6080

6181
# internal graph of the actions within this task.
6282
self.G = self.getTaskGraph()
6383

84+
6485
def organizeActions(self):
6586
'''Organizes the actions to be done by this task into the proper order
6687
based on the strategy of this type of task...
@@ -77,22 +98,33 @@ def calcDuration(self):
7798
individual actions and their order of operation.'''
7899

79100
# Does Rudy have graph-based code that can do this?
101+
102+
103+
104+
105+
106+
107+
108+
109+
80110

81111
def getTaskGraph(self):
82112
'''Generate a graph of the action dependencies.
83113
'''
84114

85115
# Create the graph
86116
G = nx.DiGraph()
87-
for item, data in self.actionList.items():
117+
for item, data in self.actions.items():
88118
for dep in data.dependencies:
89119
G.add_edge(dep, item, duration=data.duration) # Store duration as edge attribute
90120

91121
# Compute longest path & total duration
92122
longest_path = nx.dag_longest_path(G, weight='duration')
93123
longest_path_edges = list(zip(longest_path, longest_path[1:])) # Convert path into edge pairs
94-
total_duration = sum(self.actionList[node].duration for node in longest_path)
124+
125+
total_duration = sum(self.actions[node].duration for node in longest_path)
95126
return G
127+
96128

97129
def get_row(self, assets):
98130
'''Get a matrix of (cost, duration) tuples for each asset to perform this task. Will be a row in the task_asset matrix.
@@ -116,14 +148,15 @@ def get_row(self, assets):
116148
# Could look something like...
117149
'''
118150
for i, asset in enumerate(assets):
119-
for action in self.actionList: # can we do this without the double loop?
151+
for action in self.actions: # can we do this without the double loop?
120152
if asset in action.roles:
121-
action = self.actionList[asset.name]
153+
action = self.actions[asset.name]
122154
matrix[i, 0] = action.cost
123155
matrix[i, 1] = action.duration
124156
else:
125157
matrix[i, 0] = -1 # negative cost/duration means asset cannot perform task
126158
matrix[i, 1] = -1
127159
'''
128160

129-
return np.zeros((len(assets), 2)) # placeholder, replace with actual matrix
161+
return np.zeros((len(assets), 2)) # placeholder, replace with actual matrix
162+

0 commit comments

Comments
 (0)