Skip to content

How to Model Concurrent Sequences with Parallel Regions

Goal: Model a controller where two or more independent sequences run simultaneously and must synchronise at a barrier before continuing.\ Requires: How-to: Model a Sequential Machine with SSFC — understand single-region SSFCs first.


When to use parallel regions

Use PARALLEL / REGION / JOIN when:

  • Two tasks must happen concurrently (order between them does not matter, but both must complete before the controller can proceed).
  • Each task has its own state machine (its own INITIAL state, transitions, lifecycle blocks, and independent termination condition).
  • A single abort or preemption should terminate all concurrent tasks at once.

Do not use parallel regions to model a round-robin or mutually exclusive choice — those are exclusive regions within a single sequential chain.


The syntax

SUPERSTATE ConcurrentWork
  PARALLEL
    REGION regionA
      INITIAL STATE TaskA
        -- TaskA lifecycle blocks and transitions
      END_STATE
      STATE DoneA END_STATE
    END_REGION

    REGION regionB
      INITIAL STATE TaskB
        -- TaskB lifecycle blocks and transitions
      END_STATE
      STATE DoneB END_STATE
    END_REGION

    JOIN FROM DoneA, DoneB TO NextState;
  END_PARALLEL

  TRANSITION TO Aborted WHEN emergency;    -- preempts ALL regions
END_SUPERSTATE

STATE NextState
  -- entered only when both DoneA AND DoneB are simultaneously active
END_STATE

What happens scan by scan:

  1. When ConcurrentWork is entered (FORK), both TaskA and TaskB are activated simultaneously in the same scan.
  2. Each region runs its own transitions independently. In any given scan, TaskA may advance to DoneA while TaskB stays in TaskB (or vice versa).
  3. The JOIN fires in the first scan where both DoneA and DoneB are simultaneously active and the JOIN's guard (default: TRUE) is met. It exits both regions atomically in the same scan and enters NextState.
  4. If emergency goes HIGH at any point while ConcurrentWork is active, the preemption fires: all active region leaves (TaskA or DoneA; TaskB or DoneB) run their EX blocks, ConcurrentWork exits, and Aborted is entered.

A worked example: two-arm robot calibration

A robot arm has two joints (A and B) that must be calibrated independently before the arm can move:

SSFC ArmCalibration
  VAR_INPUT
    start        : BOOL;
    a_at_home    : BOOL;  a_cal_done : BOOL;
    b_at_home    : BOOL;  b_cal_done : BOOL;
    stop         : BOOL;
  END_VAR
  VAR_OUTPUT
    joint_a_cmd  : BOOL;  -- move joint A
    joint_b_cmd  : BOOL;  -- move joint B
    ready        : BOOL;  -- arm ready to operate
  END_VAR

  INITIAL STATE Idle
    TRANSITION TO Calibrate WHEN start;
  END_STATE

  SUPERSTATE Calibrate
    PARALLEL
      REGION arm_a
        INITIAL STATE A_Home
          EN:  joint_a_cmd := TRUE;
          EX:  joint_a_cmd := FALSE;
          TRANSITION TO A_Cal WHEN a_at_home;
        END_STATE
        STATE A_Cal
          EN:  joint_a_cmd := TRUE;
          EX:  joint_a_cmd := FALSE;
          TRANSITION TO A_Done WHEN a_cal_done;
        END_STATE
        STATE A_Done END_STATE
      END_REGION

      REGION arm_b
        INITIAL STATE B_Home
          EN:  joint_b_cmd := TRUE;
          EX:  joint_b_cmd := FALSE;
          TRANSITION TO B_Cal WHEN b_at_home;
        END_STATE
        STATE B_Cal
          EN:  joint_b_cmd := TRUE;
          EX:  joint_b_cmd := FALSE;
          TRANSITION TO B_Done WHEN b_cal_done;
        END_STATE
        STATE B_Done END_STATE
      END_REGION

      JOIN FROM A_Done, B_Done TO Calibrated;
    END_PARALLEL
    TRANSITION TO Idle WHEN stop;
  END_SUPERSTATE

  STATE Calibrated
    EN:  ready := TRUE;
    TRANSITION TO Idle WHEN NOT start;
  END_STATE
END_SSFC
  • While Calibrate is active, arm_a and arm_b run independently. A_Home may advance to A_Cal while B_Home has not yet reached b_at_home.
  • Once both A_Done and B_Done are active, the JOIN fires in that scan.
  • stop aborts calibration from any sub-state in either region.

Write-conflict rule

Two simultaneously-active regions must not write the same output variable in lifecycle blocks. If they do, the compiler rejects it:

-- ERROR: ssfc-cross-region-conflict
REGION arm_a
  INITIAL STATE A_Work
    DU:  status_light := TRUE;   -- arm_a writes status_light
  END_STATE
END_REGION
REGION arm_b
  INITIAL STATE B_Work
    DU:  status_light := FALSE;  -- arm_b ALSO writes status_light → conflict
  END_STATE
END_REGION

Fix options:

  1. Use separate output variables per region (light_a, light_b).
  2. Compute the combined value in the superstate's DU block (which runs while any child region is active):
    SUPERSTATE ConcurrentWork
      DU:
        status_light := A_Done.X OR B_Done.X;  -- shared derived value
      PARALLEL
        ...
      END_PARALLEL
    END_SUPERSTATE
    
  3. Use a FUNCTION_BLOCK composed with the SSFC to merge the outputs.

Priority between JOIN and preemption

When both a JOIN guard and a parent-level preemption guard are simultaneously true, the preemption wins. Preemption transitions always have higher implicit priority than child transitions (including JOIN) in the same scan.

If you need the JOIN to win over a preemption under some conditions, you can encode this explicitly using PRIORITY — but usually the correct design is to let the preemption win (that is its purpose).


JOIN with a guard

A JOIN may declare an optional WHEN guard:

JOIN FROM TaskA, TaskB TO NextState WHEN all_checks_ok;

The JOIN fires only when all of TaskA, TaskB are active and all_checks_ok is TRUE. If all_checks_ok is FALSE the machine waits in the current marking (both sources active) until the guard becomes TRUE.

A JOIN guard that is never satisfiable is flagged by C-DATA-DEAD.


What the assurance report says about parallel regions

After python -m lola arm_cal.lola, the report includes:

  • C-SAF: Token balance is checked per (region, transition) pair including the FORK (entering Calibrate activates initial states in both regions) and the JOIN (exiting A_Done and B_Done simultaneously).
  • C-INT: Both arm_a and arm_b are covered by the Calibrate superstate's preemption transition. The JOIN sources (A_Done, B_Done) are in distinct regions.
  • C-SYNC-DEAD (part of C-INT): Both A_Done and B_Done are reachable by at least one transition, so neither is a structural orphan. This does not prove they can be simultaneously active — it only checks that neither is trapped with zero inbound transitions.
  • C-REACH: A_Done and B_Done are each individually reachable from the initial marking. C-REACH does not prove co-reachability (that both can be active at the same time).
  • C-HOME: Over-approximation applies most visibly in parallel regions. A state in arm_a may receive a C-HOME PASS via a witness path that exits Calibrate via the preemption — this path is valid for arm_a's state alone, but whether arm_b is simultaneously in a position to take that path is not verified.

For parallel SSFCs, the structural claims (C-SAF, C-INT) are exact; the reachability and home claims are over-approximations.


See also