Skip to content

Commit 5d57e79

Browse files
committed
Updates for NEW dependency-offset matrices
- first off, in irma.py, we need to take the maximum dependency time offset between actions instead of the one closest to zero, which simply makes logical sense - the task_asset matrix that gets created first calculates the durations of each task-asset group assignment, and then divides them by the time_interval to be in units of periods, not hours - also, we need to make sure that we are running action.calcDurationAndCost each time a task has a new asset group assigned to it, as task.calcDuration won't recalculate the action durations for a new asset group - ALL NEW DEPENDENCY OFFSET MATRICES (at the bottom of Irma.py and implemented into the scheduler) - - we store a dictionary of AxA matrices for each task pair (skipping the same tasks as pairs) initially full of -np.inf, but then populated with offsets depending on which asset group is assigned to each task - - nothing new; it just calls findTaskDependencies for different task-asset group setups, while also saving them in units of periods instead of hours - - we also extract the proper task_dependency and dependency_types from these matrices as well and default them to 'start_start' dependencies - NEW code for calculating the total number of periods to include in the scheduler setup - The scheduler has been adjusted to account for these new dependency-offset matrices - - uses a simple _method to check the dependency offset input - - then we slightly reformat the constraint creation for dependencies and include the proper for loops to reference the proper offset out of the input matrix - - - for 'finish_start' and 'start_start' this calls for additional terms to the constraint equations that include the task-asset-group assignment as to ensure these correspond to the right offset (meaning, we can't just rely on constraining start times -- we need to include the assignments in order to determine the proper offsets) - some new updates to the scheduler successful printing (and a return dictionary), and made sure the simple example could still run - ALSO: UPDATES TO THE SCHEDULER_README AND THE SCHEDULER_TUTORIAL to reflect the new dependency-offset matrices, including more math and detailed explanations of what these constraints are doing Other small file updates: - action.py simple bug fixes for more realistic requirements and action durations - - unit conversions, proper vessel speed, print statements, loading times, etc. - to get more accurate time durations - 'winch' didn't seem to be used in the anchor_orienting requirement - task.py time_interval naming - deleted old task_asset_generator script since it's not being used and we should look at the whole code zoomed out before doing anything like that - printing/displaying things in the Irma.py run code
1 parent 76a537c commit 5d57e79

File tree

8 files changed

+490
-875
lines changed

8 files changed

+490
-875
lines changed

famodel/irma/action.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def printNotSupported(st):
271271
except:
272272
pass
273273

274-
req['bollard_pull']['max_force'] = 0.0001*mass*t2N # <<< can add a better calculation for towing force required
274+
req['bollard_pull']['max_force'] = 0.001*mass*t2N # <<< can add a better calculation for towing force required
275275

276276

277277
elif reqname == 'chain_storage': # Storage specifically for chain
@@ -1126,8 +1126,9 @@ def checkAssets(self, assets):
11261126
break # no need to check other capabilities for this requirement
11271127

11281128
if not requirements_met[req]:
1129-
print(f"Requirement '{req}' is not met by asset(s):")
1130-
# print(f"{assets}.")
1129+
print(f"Action '{self.name}': Requirement '{req}' is NOT met by asset(s):")
1130+
else:
1131+
print(f"Action '{self.name}': Requirement '{req}' can be met by asset(s)")
11311132

11321133
assignable = all(requirements_met.values())
11331134

@@ -1308,9 +1309,10 @@ def calcDurationAndCost(self):
13081309

13091310
distance = 2500 # <<< need to eventually compute distances based on positions
13101311

1311-
speed = req['assigned_assets'][0]['capabilities']['bollard_pull']['site_speed']
1312+
#speed = req['assigned_assets'][0]['capabilities']['bollard_pull']['site_speed']
1313+
speed = req['assigned_assets'][0]['transport']['transit_speed_mps']
13121314

1313-
self.durations['tow'] = distance / speed / 60 / 60 # duration [hr]
1315+
self.durations['tow'] = distance / speed / 60 # duration [hr]
13141316

13151317
elif self.type == 'transit_linehaul_self':
13161318
# TODO: RA: Needs to be updated based on new format (no roles)! - Note to dev: try to reduce (try/except) statements
@@ -1634,7 +1636,7 @@ def calcDurationAndCost(self):
16341636
req = self.requirements['anchor_onboarding']
16351637

16361638
if 'crane' in req['selected_capability']:
1637-
self.durations['load anchor by crane'] = 0.25
1639+
self.durations['load anchor by crane'] = 0.5
16381640

16391641
#elif 'windlass' in req['selected_capability']:
16401642
#self.durations['load anchor by windlass'] = 2.0
@@ -1682,7 +1684,7 @@ def calcDurationAndCost(self):
16821684
embed_speed = 1E-4*specs['power']/(np.pi/4*anchor.dd['design']['D']**2) # <<< example of more specific calculation
16831685
else:
16841686
embed_speed = 0.07 # embedment rate [m/min]
1685-
self.durations['anchor embedding'] = L*embed_speed / 60
1687+
self.durations['anchor embedding'] = L/embed_speed / 60
16861688

16871689
# 4) Connection / release (fixed)
16881690
self.durations['anchor release'] = 15/60
@@ -1700,6 +1702,9 @@ def calcDurationAndCost(self):
17001702
# previous action, which assets/objects were involved, attachments, etc.).
17011703

17021704
if 'line_handling' in self.requirements:
1705+
1706+
self.durations['mooring handling on deck'] = 0.5
1707+
17031708
req = self.requirements['line_handling'] # calculate the time for paying out line
17041709

17051710
# note: some of this code is repeated and could be put in a function
@@ -1741,7 +1746,7 @@ def calcDurationAndCost(self):
17411746
v_mpm = req['assigned_assets'][0]['capabilities']['crane']['speed']*60 # [m/min]
17421747

17431748
if v_mpm: # there is only a lowering time if a winch or crane is involved
1744-
self.durations['mooring line retrieval'] = depth/v_mpm /60 # [h]
1749+
self.durations['mooring line retrieval'] = depth*2/v_mpm /60 # [h]
17451750

17461751
# >>> tensioning could be added <<<
17471752
self.durations['generic hookup and tensioning time'] = 1
@@ -1843,7 +1848,7 @@ def calcDurationAndCost(self):
18431848

18441849
# Add cost of all assets involved for the duration of the action [$]
18451850
for asset in self.assetList:
1846-
self.costs[f"{asset['name']} day rate"] = self.duration * asset['day_rate']
1851+
self.costs[f"{asset['name']} day rate"] = self.duration/24 * asset['day_rate']
18471852

18481853
# Sum up cost
18491854
#self.cost += self.duration * sum([asset['day_rate'] for asset in self.assetList])

famodel/irma/irma.py

Lines changed: 138 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -566,21 +566,20 @@ def findTaskDependencies(task1, task2, time_interval=0.5):
566566

567567
# Calculate minimum times (rounded to nearest interval)
568568
if time_1_to_2:
569-
raw_dt_min_1_2 = min(time_1_to_2, key=abs)
569+
raw_dt_min_1_2 = max(time_1_to_2)
570570
dt_min_1_2 = np.round(raw_dt_min_1_2 / time_interval) * time_interval
571571
else:
572572
dt_min_1_2 = -np.inf
573573

574574
if time_2_to_1:
575-
raw_dt_min_2_1 = min(time_2_to_1, key=abs)
575+
raw_dt_min_2_1 = max(time_2_to_1)
576576
dt_min_2_1 = np.round(raw_dt_min_2_1 / time_interval) * time_interval
577577
else:
578578
dt_min_2_1 = -np.inf
579579

580580
if dt_min_1_2 + dt_min_2_1 > 0:
581581
print(f"The timing between these two tasks seems to be impossible...")
582582

583-
#breakpoint()
584583
return dt_min_1_2, dt_min_2_1
585584

586585

@@ -714,7 +713,7 @@ def implementStrategy_staged(sc):
714713

715714
display = 1
716715

717-
sc = Scenario() # class instance holding most of the info
716+
sc = Scenario(display=display) # class instance holding most of the info
718717

719718

720719
# ----- Create the interrelated actions (including their individual requirements) -----
@@ -762,7 +761,7 @@ def implementStrategy_staged(sc):
762761
# ----- Do some graph analysis -----
763762

764763
#G = sc.visualizeActions()
765-
G = sc.visualizeActionsHierarchy()
764+
#G = sc.visualizeActionsHierarchy()
766765

767766

768767
# ----- Generate tasks (sequences of Actions following specific strategies) -----
@@ -771,7 +770,7 @@ def implementStrategy_staged(sc):
771770
# Call one of the task strategy implementers, which will create the tasks
772771
implementStrategy_staged(sc)
773772

774-
sc.tasks['install_all_anchors'].checkAssets([sc.vessels['AHTS_alpha'], sc.vessels['MPSV_01']], display=1)
773+
time_interval = 0.25
775774

776775
# ----- Try assigning assets to the tasks -----
777776
print('===== Assigning Asset Groups to Tasks =====')
@@ -793,7 +792,7 @@ def implementStrategy_staged(sc):
793792
# Calculation durations of the actions, and then of the task
794793
for a in task.actions.values():
795794
a.calcDurationAndCost()
796-
task.calcDuration()
795+
task.calcDuration(time_interval=time_interval)
797796

798797

799798
# Example task time adjustment and plot
@@ -802,17 +801,17 @@ def implementStrategy_staged(sc):
802801

803802

804803
# ----- Quantify the dependency offsets between tasks -----
805-
806-
time_interval = 0.25
807804

808805
dt_min = sc.figureOutTaskRelationships(time_interval=time_interval)
809806

810807

811808

812809
# ----- Call the scheduler -----
813810

811+
# TASKS
814812
tasks_scheduler = list(sc.tasks.keys())
815813

814+
# ASSETS
816815
for asset in sc.vessels.values():
817816
if 'name' in asset:
818817
if asset['name'] == 'AHTS_alpha':
@@ -823,16 +822,16 @@ def implementStrategy_staged(sc):
823822
asset['max_weather'] = asset['transport']['Hs_m']
824823
assets_scheduler = list(sc.vessels.values())
825824

825+
# ASSET GROUPS
826826
# >>>>> TODO: make this automated to find all possible combinations of "realistic" asset groups
827827
asset_groups_scheduler = [
828828
{'group1': ['AHTS_alpha']},
829829
{'group2': ['MPSV_01']},
830-
#{'group2': ['CSV_A']},
831-
#{'group3': ['AHTS_alpha', 'CSV_A', 'HL_Giant']}
832830
{'group3': ['AHTS_alpha', 'MPSV_01']}
833831
]
834832

835-
task_asset_matrix_scheduler = np.zeros([len(tasks_scheduler), len(asset_groups_scheduler), 2], dtype=int)
833+
# TASK-ASSET-MATRIX
834+
task_asset_matrix_scheduler = np.zeros([len(tasks_scheduler), len(asset_groups_scheduler), 2])
836835
for i,task in enumerate(sc.tasks.values()):
837836
for j,asset_group in enumerate(asset_groups_scheduler):
838837
# Extract asset list from the dictionary - values() returns a list containing one list
@@ -843,48 +842,138 @@ def implementStrategy_staged(sc):
843842
task_asset_matrix_scheduler[i,j] = (-1, -1)
844843
else:
845844
task.assignAssets(asset_list)
846-
task.calcDuration(duration_interval=time_interval)
845+
for a in task.actions.values():
846+
a.calcDurationAndCost()
847+
task.calcDuration(time_interval=time_interval)
847848
task.calcCost()
848-
duration_int = int(round(task.duration / time_interval))
849+
duration_int = int(np.ceil(task.duration / time_interval))
849850
task_asset_matrix_scheduler[i,j] = (task.cost, duration_int)
850851
task.clearAssets()
852+
853+
854+
# DEPENDENCIES
855+
856+
# Create dependency offset matrices for each pair of tasks (not the same task in pairs)
857+
# All rows represent the dependent task and all columns represent the prerequisite task
858+
# Each individual row or column represents a different asset group
859+
# Values in the matrices are the minimum time offset required between those tasks with those asset assignments
860+
# For example, if there are 3 tasks and 3 asset groups:
861+
# if task2 depends on task1, if task2 uses the second asset group, and if task1 uses the third asset group,
862+
# then there will be a non-inf value in the [1,2] (python-indexing) spot for the time offset
863+
864+
dependency_offset_matrices = {}
865+
task_list = list(sc.tasks.values())
866+
task_names = list(sc.tasks.keys())
867+
868+
# Initialize matrices for each dependency pair ("dependent_task->prerequisite_task")
869+
for i, task1 in enumerate(task_list):
870+
for k, task2 in enumerate(task_list):
871+
if i != k: # Don't compare a task with itself
872+
dep_key = f"{task_names[k]}->{task_names[i]}" # "task2->task1" = "dependent->prerequisite"
873+
dependency_offset_matrices[dep_key] = np.full((len(asset_groups_scheduler), len(asset_groups_scheduler)), -np.inf)
874+
875+
# Calculate offsets for each valid task-asset-group combination
876+
for i, task1 in enumerate(task_list):
877+
for j, ag1 in enumerate(asset_groups_scheduler):
878+
# Get the list of vessels for this asset group
879+
asset_list1 = [sc.vessels[name] for name in list(ag1.values())[0]]
851880

881+
# Check if this asset group can perform task1
882+
if task1.checkAssets(asset_list1, display=0)[0]:
883+
# Temporarily assign assets and calculate durations
884+
task1.assignAssets(asset_list1)
885+
for a in task1.actions.values():
886+
a.calcDurationAndCost()
887+
task1.calcDuration(time_interval=time_interval)
888+
889+
# Check dependencies with all other tasks
890+
for k, task2 in enumerate(task_list):
891+
if i == k: # Skip self-comparison
892+
continue
893+
894+
for l, ag2 in enumerate(asset_groups_scheduler):
895+
asset_list2 = [sc.vessels[name] for name in list(ag2.values())[0]]
896+
897+
# Check if this asset group can perform task2
898+
if task2.checkAssets(asset_list2, display=0)[0]:
899+
# Temporarily assign assets and calculate durations
900+
task2.assignAssets(asset_list2)
901+
for a in task2.actions.values():
902+
a.calcDurationAndCost()
903+
task2.calcDuration(time_interval=time_interval)
904+
905+
# Calculate the minimum time offset from task1 to task2
906+
dt_min_1_to_2, dt_min_2_to_1 = findTaskDependencies(task1, task2, time_interval=time_interval)
907+
908+
# Store in the appropriate matrix (and convert from hours to periods)
909+
dep_key_k_to_i = f"{task_names[k]}->{task_names[i]}"
910+
if dt_min_1_to_2 != -np.inf:
911+
dependency_offset_matrices[dep_key_k_to_i][l, j] = int(np.ceil(dt_min_1_to_2 / time_interval))
912+
else:
913+
dependency_offset_matrices[dep_key_k_to_i][l, j] = -np.inf
914+
915+
dep_key_i_to_k = f"{task_names[i]}->{task_names[k]}"
916+
if dt_min_2_to_1 != -np.inf:
917+
dependency_offset_matrices[dep_key_i_to_k][j, l] = int(np.ceil(dt_min_2_to_1 / time_interval))
918+
else:
919+
dependency_offset_matrices[dep_key_i_to_k][j, l] = -np.inf
920+
921+
task2.clearAssets()
922+
923+
task1.clearAssets()
924+
925+
852926

927+
# Calculate task_dependencies and dependency_types from the dependency_offset_matrices
928+
853929
task_dependencies = {}
854930
dependency_types = {}
855-
offsets = {}
856-
for i, task1 in enumerate(sc.tasks.values()):
857-
for j, task2 in enumerate(sc.tasks.values()):
858-
offset = dt_min[i,j]
859-
if i != j and offset != -np.inf:
860-
if task2.name not in task_dependencies:
861-
task_dependencies[task2.name] = []
862-
task_dependencies[task2.name].append(task1.name)
863-
dependency_types[task1.name + '->' + task2.name] = 'start_start'
864-
offsets[task1.name + '->' + task2.name] = offset / time_interval
931+
932+
for dep_key, matrix in dependency_offset_matrices.items():
933+
# Check if there are any valid offsets (non -inf) in this matrix
934+
if np.any(matrix != -np.inf):
935+
# Parse the dependency key: "dependent_task->prerequisite_task"
936+
dependent_task, prerequisite_task = dep_key.split('->')
937+
938+
# Add to task_dependencies
939+
if dependent_task not in task_dependencies:
940+
task_dependencies[dependent_task] = []
941+
task_dependencies[dependent_task].append(prerequisite_task)
942+
943+
# Set dependency type based on task characteristics (default is 'start_start') - can update this later with other dependency types
944+
dependency_types[dep_key] = 'start_start'
865945

866-
for task in sc.tasks.values():
867-
task.calcDuration() # ensure the durations of each task are calculated
868-
869-
task_start_times = {}
870-
task_finish_times = {}
871-
task_list = list(sc.tasks.keys())
872-
873-
for task_name in task_list:
874-
# Find earliest start time based on dependencies
875-
earliest_start = 0
876-
for i, t1_name in enumerate(task_list):
877-
j = task_list.index(task_name)
878-
if i != j and dt_min[i, j] != -np.inf:
879-
# This task depends on t1
880-
earliest_start = max(earliest_start,
881-
task_finish_times.get(t1_name, 0) + dt_min[i, j])
882-
883-
task_start_times[task_name] = earliest_start
884-
task_finish_times[task_name] = earliest_start + sc.tasks[task_name].duration
885-
886-
weather = np.arange(0, max(task_finish_times.values())+ time_interval, time_interval)
887-
weather = [int(x) for x in np.ones(int(max(task_finish_times.values()) / time_interval), dtype=int)]
946+
947+
948+
949+
# PERIODS
950+
# Calculate total number of periods needed for the scheduler
951+
# For each task, find the maximum duration across all valid asset group assignments
952+
total_task_duration = 0
953+
for i, task in enumerate(sc.tasks.values()):
954+
# Get durations for this task across all asset groups (index 1 is duration in periods)
955+
task_durations = task_asset_matrix_scheduler[i, :, 1]
956+
# Filter out invalid assignments (negative or zero duration)
957+
valid_durations = task_durations[task_durations > 0]
958+
if len(valid_durations) > 0:
959+
max_duration = np.max(valid_durations)
960+
total_task_duration += max_duration
961+
# Find maximum offset from all dependency matrices (ignoring -inf values and negative values)
962+
max_offsets_sum = 0
963+
for dep_key, matrix in dependency_offset_matrices.items():
964+
valid_offsets = matrix[matrix != -np.inf]
965+
if len(valid_offsets) > 0 and max(valid_offsets) >= 0:
966+
max_offsets_sum += np.max(valid_offsets)
967+
968+
total_duration = total_task_duration + max_offsets_sum
969+
# total_duration is already in periods, so just convert to integer
970+
num_periods = int(np.ceil(total_duration))
971+
972+
973+
974+
975+
976+
weather = [int(x) for x in np.ones(num_periods, dtype=int)]
888977
'''
889978
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,
890979
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,
@@ -896,9 +985,9 @@ def implementStrategy_staged(sc):
896985
assets=assets_scheduler,
897986
asset_groups=asset_groups_scheduler,
898987
task_asset_matrix=task_asset_matrix_scheduler,
899-
task_dependencies=task_dependencies,
900-
dependency_types=dependency_types,
901-
offsets=offsets,
988+
task_dependencies=task_dependencies, # Derived from dependency_offset_matrices
989+
dependency_types=dependency_types, # Derived from dependency_offset_matrices
990+
offsets=dependency_offset_matrices, # Use asset-group-specific matrices
902991
weather=weather,
903992
period_duration=time_interval,
904993
wordy=1

famodel/irma/requirements.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ anchor_orienting:
119119
description: "Capability to orient an anchor during installation."
120120
objects: [anchor]
121121
capabilities:
122-
- winch
122+
#- winch
123123
- rov
124124
- divers
125125

0 commit comments

Comments
 (0)