Skip to content

Commit db71f59

Browse files
committed
Completed StrategistService with API points.
1 parent 81fd541 commit db71f59

3 files changed

Lines changed: 94 additions & 35 deletions

File tree

alchemiscale/storage/statestore.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2646,6 +2646,41 @@ def get_transformation_tasks(
26462646
for t in tasks
26472647
}
26482648

2649+
def get_transformation_actioned_tasks(
2650+
self,
2651+
transformation: ScopedKey,
2652+
taskhub: ScopedKey,
2653+
) -> list[ScopedKey]:
2654+
"""Get all Tasks for a Transformation that are actioned by the given TaskHub.
2655+
2656+
Parameters
2657+
----------
2658+
transformation
2659+
ScopedKey of the Transformation to retrieve actioned Tasks for.
2660+
taskhub
2661+
ScopedKey of the TaskHub to check for actioned Tasks.
2662+
2663+
Returns
2664+
-------
2665+
tasks
2666+
List of Task ScopedKeys that perform the given Transformation and are
2667+
actioned by the given TaskHub.
2668+
"""
2669+
q = """
2670+
MATCH (th:TaskHub {_scoped_key: $taskhub})-[:ACTIONS]->(task:Task),
2671+
(task)-[:PERFORMS]->(trans:Transformation|NonTransformation {_scoped_key: $transformation})
2672+
RETURN task._scoped_key
2673+
"""
2674+
2675+
with self.transaction() as tx:
2676+
results = tx.run(
2677+
q,
2678+
transformation=str(transformation),
2679+
taskhub=str(taskhub),
2680+
).to_eager_result()
2681+
2682+
return [ScopedKey.from_str(record["task._scoped_key"]) for record in results.records]
2683+
26492684
def get_task_transformation(
26502685
self,
26512686
task: ScopedKey,

alchemiscale/strategist/service.py

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ def _weights_to_task_counts(
206206
def _execute_strategy(
207207
self,
208208
network_sk: ScopedKey,
209-
strategy_sk: ScopedKey,
210209
strategy_state: StrategyState
211210
) -> StrategyState:
212211
"""Execute a single strategy and return updated state."""
@@ -273,46 +272,73 @@ def _execute_strategy(
273272
strategy_state.task_scaling,
274273
)
275274

275+
taskhub_sk = self.n4js.get_taskhub(network_sk)
276+
276277
# Set task counts for each transformation
277278
for transformation_sk, target_count in task_counts.items():
278-
current_tasks = self.n4js.get_transformation_tasks(transformation_sk)
279-
actioned_tasks = [
280-
task_sk for task_sk in current_tasks
281-
if self.n4js.get_task(task_sk).status in [TaskStatusEnum.waiting, TaskStatusEnum.running]
282-
]
283-
current_count = len(actioned_tasks)
279+
280+
actioned_tasks = self.n4js.get_transformation_actioned_tasks(transformation_sk, taskhub_sk)
281+
actioned_count = len(actioned_tasks)
284282

285-
if target_count > current_count:
286-
# Create new tasks
287-
for _ in range(target_count - current_count):
288-
self.n4js.create_task(transformation_sk)
283+
if target_count > actioned_count:
284+
# Action additional tasks, creating them as necessary
285+
required = target_count - actioned_count
286+
tasks_to_action = []
287+
288+
# Get existing actionable tasks for the transformation
289+
transformation_tasks = self.n4js.get_transformation_tasks(transformation_sk)
290+
actionable_tasks_status = {
291+
task_sk: status for task_sk, status in zip(transformation_tasks, self.n4js.get_task_status(transformation_tasks))
292+
if status in [TaskStatusEnum.waiting, TaskStatusEnum.running] and task_sk not in actioned_tasks
293+
}
294+
295+
# Add actionable tasks not already actioned, starting with those that are already running
296+
for task_sk in actionable_tasks_status:
297+
status = actionable_tasks_status[task_sk]
298+
if status == TaskStatusEnum.running:
299+
tasks_to_action.append(task_sk)
300+
if len(tasks_to_action) >= required:
301+
break
302+
303+
# If we still need more, add actionable tasks that are waiting
304+
if len(tasks_to_action) < required:
305+
for task_sk in actionable_tasks_status:
306+
status = actionable_tasks_status[task_sk]
307+
if status == TaskStatusEnum.waiting:
308+
tasks_to_action.append(task_sk)
309+
if len(tasks_to_action) >= required:
310+
break
311+
312+
# Create new tasks if needed
313+
if len(tasks_to_action) < required:
314+
new_tasks = self.n4js.create_tasks([transformation_sk] * (required - len(tasks_to_action)))
315+
tasks_to_action.extend(new_tasks)
316+
317+
self.n4js.action_tasks(tasks_to_action, taskhub_sk)
289318

290-
elif target_count < current_count and strategy_state.mode == StrategyModeEnum.full:
291-
# Cancel excess tasks (prioritize unclaimed ones)
292-
excess = current_count - target_count
319+
elif target_count < actioned_count and strategy_state.mode == StrategyModeEnum.full:
320+
# Cancel excess tasks
321+
excess = actioned_count - target_count
293322
tasks_to_cancel = []
323+
324+
actioned_status = self.n4js.get_task_status(actioned_tasks)
294325

295-
# First cancel unclaimed tasks
296-
for task_sk in actioned_tasks:
297-
task = self.n4js.get_task(task_sk)
298-
if task.status == TaskStatusEnum.waiting and task.claim is None:
326+
# First cancel waiting tasks
327+
for task_sk, status in zip(actioned_tasks, actioned_status):
328+
if status == TaskStatusEnum.waiting:
299329
tasks_to_cancel.append(task_sk)
300330
if len(tasks_to_cancel) >= excess:
301331
break
302332

303-
# Then cancel claimed but not running tasks if needed
333+
# Then cancel running tasks if needed
304334
if len(tasks_to_cancel) < excess:
305-
for task_sk in actioned_tasks:
306-
if task_sk not in tasks_to_cancel:
307-
task = self.n4js.get_task(task_sk)
308-
if task.status == TaskStatusEnum.waiting:
309-
tasks_to_cancel.append(task_sk)
310-
if len(tasks_to_cancel) >= excess:
311-
break
335+
for task_sk, status in zip(actioned_tasks, actioned_status):
336+
if status == TaskStatusEnum.running:
337+
tasks_to_cancel.append(task_sk)
338+
if len(tasks_to_cancel) >= excess:
339+
break
312340

313-
# Cancel the selected tasks
314-
for task_sk in tasks_to_cancel:
315-
self.n4js.cancel_task(task_sk)
341+
self.n4js.cancel_tasks(tasks_to_cancel, taskhub_sk)
316342

317343
# Update strategy state
318344
strategy_state.last_iteration = datetime.utcnow()
@@ -327,10 +353,11 @@ def _execute_strategy(
327353
# Strategy execution failed
328354
logger.exception(f"Strategy execution failed for network {network_sk}")
329355

356+
strategy_state.last_iteration = datetime.utcnow()
357+
strategy_state.last_iteration_result_count = current_result_count
330358
strategy_state.status = StrategyStatusEnum.error
331-
strategy_state.exception = (type(e).__name__, str(e))
359+
strategy_state.exception = (e.__class__.__qualname__, str(e))
332360
strategy_state.traceback = traceback.format_exc()
333-
strategy_state.last_iteration = datetime.utcnow()
334361
strategy_state.iterations += 1
335362

336363
return strategy_state
@@ -353,7 +380,7 @@ def cycle(self):
353380
with ProcessPoolExecutor(max_workers=self.max_workers) as executor:
354381
# Submit all strategy executions
355382
future_to_network = {
356-
executor.submit(self._execute_strategy, network_sk, strategy_sk, strategy_state): network_sk
383+
executor.submit(self._execute_strategy, network_sk, strategy_state): network_sk
357384
for network_sk, strategy_sk, strategy_state in ready_strategies
358385
}
359386
network_to_future = {value: key for key, value in future_to_network.items()}

docs/strategy_guide.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
Using Strategies with alchemiscale
55
##################################
66

7-
.. contents:: Contents
8-
:depth: 2
9-
107
Overview
118
========
129

0 commit comments

Comments
 (0)