Scheduler

Overview

A Scheduler is used to generate the order in which the Components of a Composition are executed. By default, a Scheduler executes Components in an order determined by the pattern of Projections among the Mechanisms in the Composition, with each Mechanism executed once per PASS through the Composition. For example, in a System in which a Mechanism A projects to a Mechanism B that projects to a Mechanism C, A will execute first followed by B, and then C in each PASS through the System. However, a Scheduler can be used to implement more complex patterns of execution, by specifying Conditions that determine when and how many times individual Components execute, and whether and how this depends on the execution of other Components. Any executable Component in a Composition can be assigned a Condition, and Conditions can be combined in arbitrary ways to generate any pattern of execution of the Components in a Composition that is logically possible.

Note

In general, Mechanisms are the Components of a Composition that are most commonly associated with Conditions, and assigned to a Scheduler for execution. However, in some circumstances, Projections can also be assigned for execution (e.g., during learning to insure that MappingProjections are updated in the proper order).

Creating a Scheduler

A Scheduler can be created explicitly using its constructor. However, more commonly it is created automatically for a Composition when it is created. When creating a Scheduler explicitly, the set of Components to be executed and their order must be specified in the Scheduler’s constructor using one the following:

  • a System in the system argument - if a System is specified, the Scheduler is created using the Components in the System’s execution_list and an order of execution specified by the dependencies among the Components in its execution_graph.
  • a graph specification dictionary in the graph argument - each entry of the dictionary must be a Component of a Composition, and the value of each entry must be a set of zero or more Components that project directly to the key. The graph must be acyclic; an error is generated if any cycles (e.g., recurrent dependencies) are detected. The Scheduler computes a toposort from the graph that is used as the default order of executions, subject to any Condition`s that have been specified (see `below).

If both a System and a graph are specified, the System takes precedence, and the graph is ignored.

Conditions can be added to a Scheduler when it is created by specifying a ConditionSet (a set of Conditions) in the condition_set argument of its constructor. Individual Conditions and/or ConditionSets can also be added after the Scheduler has been created, using its add_condition and add_condition_set methods, respectively.

Algorithm

When a Scheduler is created, it constructs a consideration_queue: a list of consideration_sets that defines the order in which Components are eligible to be executed. This is based on the pattern of projections among them specified in the System, or on the dependencies specified in the graph specification dictionary, whichever was provided in the Scheduler’s constructor. Each consideration_set is a set of Components that are eligible to execute at the same time/TIME_STEP (i.e., that appear at the same “depth” in a sequence of dependencies, and among which there are no dependencies). The first consideration_set consists of only ORIGIN Mechanisms. The second consists of all Components that receive Projections from the Mechanisms in the first consideration_set. The third consists of Components that receive Projections from Components in the first two consideration_sets, and so forth. When the Scheduler is run, it uses the consideration_queue to determine which Components are eligible to execute in each TIME_STEP of a PASS, and then evaluates the Condition associated with each Component in the current consideration_set to determine which should actually be assigned for execution.

Pseudocode:

consideration_queue <- list(toposort(graph))

reset TimeScale.TRIAL counters
while TimeScale.TRIAL termination conditions are not satisfied:
    reset TimeScale.PASS counters
    cur_index <- 0

    while TimeScale.TRIAL termination conditions are not satisfied
          and cur_index < len(consideration_queue):

        cur_consideration_set <- consideration_queue[cur_index]
        do:
            cur_consideration_set_has_changed <- False
            for cur_node in cur_consideration_set:
                if  cur_node not in cur_time_step
                    and cur_node`s Condition is satisfied:

                    cur_consideration_set_has_changed <- True
                    add cur_node to cur_time_step
                    increment execution and time counters
        while cur_consideration_set_has_changed

        if cur_time_step is not empty:
            yield cur_time_step

        increment cur_index
        increment time counters

Execution

When a Scheduler is run, it provides a set of Components that should be run next, based on their dependencies in the System or graph specification dictionary, and any Conditions, specified in the Scheduler’s constructor. For each call to the run method, the Scheduler sequentially evaluates its consideration_sets in their order in the consideration_queue. For each set, it determines which Components in the set are allowed to execute, based on whether their associated Condition has been met. Any Component that does not have a Condition explicitly specified is assigned the Condition Always, that allows it to execute any time it is under consideration. All of the Components within a consideration_set that are allowed to execute comprise a TIME_STEP of execution. These Components are considered as executing simultaneously.

Note

The ordering of the Components specified within a TIME_STEP is arbitrary (and is irrelevant, as there are no graph dependencies among Components within the same consideration_set). However, the execution of a Component within a time_step may trigger the execution of another Component within its consideration_set, as in the example below:

    C
  ↗ ↖
 A   B

scheduler.add_condition(B, pnl.EveryNCalls(A, 2))
scheduler.add_condition(C, pnl.EveryNCalls(B, 1))

time steps: [{A}, {A, B}, {C}, ...]

Since there are no graph dependencies between A and B, they may execute in the same TIME_STEP. Morever, A and B are in the same consideration_set. Since B is specified to run every two times A runs, A’s second execution in the second TIME_STEP allows B to run within that TIME_STEP, rather than waiting for the next PASS.

For each TIME_STEP, the Scheduler evaluates whether any specified termination Conditions have been met, and terminates if so. Otherwise, it returns the set of Components that should be executed in the current TIME_STEP. Each subsequent call to the run method returns the set of Components in the following TIME_STEP.

Processing of all of the consideration_sets in the consideration_queue constitutes a PASS of execution, over which every Component in the Composition has been considered for execution. Subsequent calls to the run method cycle back through the consideration_queue, evaluating the consideration_sets in the same order as previously. Different subsets of Components within the same consideration_set may be assigned to execute on each PASS, since different Conditions may be satisfied.

The Scheduler continues to make PASS`es through the `consideration_queue until a termination Condition is satisfied. If no termination Conditions are specified, the Scheduler terminates a TRIAL when every Component has been specified for execution at least once (corresponding to the AllHaveRun Condition). However, other termination Conditions can be specified, that may cause the Scheduler to terminate a TRIAL earlier or later (e.g., when the Condition for a particular Component or set of Components is met). When the Scheduler terminates a TRIAL, the Composition begins processing the next input specified in the call to its run method. Thus, a TRIAL is defined as the scope of processing associated with a given input to the Composition.

Termination Conditions

Termination conditions are Conditions that specify when the open-ended units of time - TRIAL and RUN - have ended. By default, the termination condition for a TRIAL is AllHaveRun, which is satisfied when all Components have run at least once within the trial, and the termination condition for a RUN is when all of its constituent trials have terminated. These defaults may be overriden when running a Composition, by passing a dictionary mapping TimeScales to Conditions in the termination_processing argument of a call to Composition.run (to terminate the execution of processing), or its termination_learning argument to terminate the execution of learning:

System.run(
    ...,
    termination_processing={TimeScale.TRIAL: WhenFinished(ddm)}
    )

Examples

Please see Condition for a list of all supported Conditions and their behavior.

  • Basic phasing in a linear process:

    >>> import psyneulink as pnl
    
    >>> A = pnl.TransferMechanism(function=pnl.Linear(), name='A')
    >>> B = pnl.TransferMechanism(function=pnl.Linear(), name='B')
    >>> C = pnl.TransferMechanism(function=pnl.Linear(), name='C')
    
    >>> p = pnl.Process(
    ...     pathway=[A, B, C],
    ...     name = 'p'
    ... )
    >>> s = pnl.System(
    ...     processes=[p],
    ...     name='s'
    ... )
    >>> my_scheduler = pnl.Scheduler(system=s)
    
    >>> # implicit condition of Always for A
    >>> my_scheduler.add_condition(B, pnl.EveryNCalls(A, 2))
    >>> my_scheduler.add_condition(C, pnl.EveryNCalls(B, 3))
    
    >>> # implicit AllHaveRun Termination condition
    >>> execution_sequence = list(my_scheduler.run())
    >>> execution_sequence
    [{(TransferMechanism A)}, {(TransferMechanism A)}, {(TransferMechanism B)}, {(TransferMechanism A)}, {(TransferMechanism A)}, {(TransferMechanism B)}, {(TransferMechanism A)}, {(TransferMechanism A)}, {(TransferMechanism B)}, {(TransferMechanism C)}]
    
  • Alternate basic phasing in a linear process:

    >>> A = pnl.TransferMechanism(function=pnl.Linear(), name='A')
    >>> B = pnl.TransferMechanism(function=pnl.Linear(), name='B')
    
    >>> p = pnl.Process(
    ...     pathway=[A, B],
    ...     name = 'p'
    ... )
    >>> s = pnl.System(
    ...     processes=[p],
    ...     name='s'
    ... )
    >>> my_scheduler = pnl.Scheduler(system=s)
    
    >>> my_scheduler.add_condition(
    ...     A,
    ...     pnl.Any(
    ...         pnl.AtPass(0),
    ...         pnl.EveryNCalls(B, 2)
    ...     )
    ... )
    
    >>> my_scheduler.add_condition(
    ...     B,
    ...     pnl.Any(
    ...         pnl.EveryNCalls(A, 1),
    ...         pnl.EveryNCalls(B, 1)
    ...     )
    ... )
    
    >>> termination_conds = {
    ...     pnl.TimeScale.TRIAL: pnl.AfterNCalls(B, 4, time_scale=pnl.TimeScale.TRIAL)
    ... }
    >>> execution_sequence = list(my_scheduler.run(termination_conds=termination_conds))
    
    execution_sequence: [A, B, B, A, B, B]
    
  • Basic phasing in two processes:

    >>> A = pnl.TransferMechanism(function=pnl.Linear(), name='A')
    >>> B = pnl.TransferMechanism(function=pnl.Linear(), name='B')
    >>> C = pnl.TransferMechanism(function=pnl.Linear(), name='C')
    
    >>> p = pnl.Process(
    ...         pathway=[A, C],
    ...         name = 'p'
    ... )
    >>> q = pnl.Process(
    ...         pathway=[B, C],
    ...         name = 'q'
    ... )
    >>> s = pnl.System(
    ...         processes=[p, q],
    ...         name='s'
    ... )
    >>> my_scheduler = pnl.Scheduler(system=s)
    
    >>> my_scheduler.add_condition(A, pnl.EveryNPasses(1))
    >>> my_scheduler.add_condition(B, pnl.EveryNCalls(A, 2))
    >>> my_scheduler.add_condition(
    ...     C,
    ...     pnl.Any(
    ...         pnl.AfterNCalls(A, 3),
    ...         pnl.AfterNCalls(B, 3)
    ...     )
    ... )
    
    >>> termination_conds = {
    ...     pnl.TimeScale.TRIAL: pnl.AfterNCalls(C, 4, time_scale=pnl.TimeScale.TRIAL)
    ... }
    >>> execution_sequence = list(my_scheduler.run(termination_conds=termination_conds))
    
    execution_sequence: [A, {A,B}, A, C, {A,B}, C, A, C, {A,B}, C]
    

Class Reference

class psyneulink.scheduling.scheduler.Scheduler(system=None, composition=None, graph=None, condition_set=None, termination_conds=None)

Generates an order of execution for Components in a Composition or graph specification dictionary, possibly determined by a set of Conditions.

Parameters:
  • system (System) – specifies the Components to be ordered for execution, and any dependencies among them, based on the System’s execution_graph and execution_list.
  • condition_set (ConditionSet) – set of Conditions that specify when individual Components in system execute and any dependencies among them
  • graph (Dict[Component: set(Component)]) – a graph specification dictionary - each entry of the dictionary must be a Component, and the value of each entry must be a set of zero or more Components that project directly to the key.
condition_set

ConditionSet – the set of Conditions the Scheduler uses when running

execution_list

list – the full history of time steps the Scheduler has produced

consideration_queue

list – a list form of the Scheduler’s toposort ordering of its nodes

termination_conds

Dict[TimeScale: Condition] – a mapping from TimeScales to Conditions that, when met, terminate the execution of the specified TimeScale.

times

Dict[TimeScale: Dict[TimeScale: int]] – a structure counting the number of occurrences of a certain TimeScale within the scope of another TimeScale. For example, times[TimeScale.RUN][TimeScale.PASS] is the number of PASS`es that have occurred in the current `RUN that the Scheduler is scheduling at the time it is accessed

clock

Clock – a Clock object that stores the current time in this Scheduler

add_condition(owner, condition)
Parameters:
add_condition_set(conditions)
Parameters:conditions – a dict mapping Component`s to `Condition`s, which can be added later with `add_condition
run(termination_conds=None, execution_id=None, base_execution_id=None)

run is a python generator, that when iterated over provides the next TIME_STEP of executions at each iteration

Parameters:termination_conds – (dict) - a mapping from TimeScale`s to `Condition`s that when met terminate the execution of the specified `TimeScale