OCSMP Model Builder Intermediate study materials
OCSMP Model Builder Intermediate study resources

State-machine precedence and activity token deadlock in an infusion pump controller

OCSMP Model Builder Intermediate · Put your knowledge to work in context.

All case studies

State-machine precedence and activity token deadlock in an infusion pump controller

An infusion pump controller has both an activity model and a state machine. Activity DeliverDose contains actions ReadPressure, CheckOcclusion and DriveMotor. CheckOcclusion has one input pin fed by an object flow from a central buffer node latestPressure, which is written only by ReadPressure; CheckOcclusion also receives a control flow from a decision node that bypasses ReadPressure when the reservoir is empty. State machine PumpModes has Idle, Priming, Delivering (composite, with substates Pressurizing and Holding) and Fault and Paused. Two transitions leave the composite state on signal OcclusionDetected: Delivering to Fault with no guard, and Delivering.Holding to Paused with guard [pressure < 200 mmHg]. During simulation the model hangs whenever the reservoir-empty branch is taken, with the report that CheckOcclusion's input pin has no token. Separately, DriveMotor is allocated to MotorDriver, and the whole DeliverDose activity is allocated both to MotorDriver and to PumpController, two blocks with no other relationship.

1. With pressure at 180 mmHg and the machine in Delivering.Holding, which transition fires on OcclusionDetected, and why? How would you make the behaviour deterministic and complete?

Read the worked answer

UML resolves conflicting transitions by hierarchy: transitions whose source is the innermost active state are evaluated first, and transitions from enclosing states are only considered if none of the nested ones is enabled. The active configuration is Delivering.Holding, so Holding to Paused with guard [pressure < 200 mmHg] is tested first; at 180 mmHg the guard is true and that transition fires. The unguarded Delivering to Fault transition never gets a look.

That precedence also hides two defects. First, the nested guard is not the only thing selecting behaviour: at 250 mmHg the nested transition is disabled and the unguarded one fires, so any pressure above 200 mmHg produces Fault, which may or may not be the clinical intent. Second, the unguarded transition is permanently enabled while Delivering is active, so it acts as a catch-all that no one documented. If both transitions had been sourced on Delivering itself, selection would be genuinely nondeterministic, because UML defines precedence only through state hierarchy, not between alternatives at the same level.

Make it deterministic by making guards mutually exclusive and complete, and by routing the event through a choice pseudostate inside Delivering that dispatches to Paused or Fault on pressure bands, with an explicit default branch for missing or stale pressure readings. If OcclusionDetected can also arrive during Priming, add it to the deferred-event list rather than letting it be silently discarded. Finally, name the thresholds as value properties so the guards reference parameters instead of literals.

2. Explain the hang in activity terms. Why does a control flow not satisfy the input pin, and what are the viable modelling fixes?

Read the worked answer

Two token types are being confused. A control flow delivers a control token that enables or triggers the target action but carries no data. An object flow delivers object tokens into pins, and an action cannot start until every input pin holds at least the lower-bound number of tokens required. CheckOcclusion's data comes only from the object flow out of latestPressure, and latestPressure only receives a token when ReadPressure executes.

On the reservoir-empty path the decision sends control around ReadPressure and straight to CheckOcclusion. The control token arrives, the action is enabled, but the pressure pin stays empty, so the action can never start. The model is reporting a genuine specification hole, not a tool bug: the process requires a pressure value on a path that never produces one.

Fixes, in order of preference. (i) Make the empty path still supply data: route a default or last-known pressure into latestPressure via an object flow so every path to CheckOcclusion provides a token, and represent "reservoir empty" as a value of the pressure reading rather than as a missing reading. (ii) If absence is genuinely part of the domain, lower the pin's multiplicity to 0..1 and handle the no-value case explicitly inside CheckOcclusion, turning an indefinite wait into a specified branch. (iii) For continuously sampled pressure, model the parameter as streaming rather than as a discrete token. Do not take the common shortcut of drawing a control flow directly onto the pin; tokens of the two kinds are not interchangeable and the mismatch will resurface at the next tool.

3. Assess the allocation. What does «allocate» assert, is allocating DeliverDose to two unrelated blocks acceptable, and how does allocation differ from «satisfy» and «verify»?

Read the worked answer

«allocate» is a general mapping dependency: the source is any element at one level of abstraction, the target any element at another, used for function-to-structure and other cross-abstraction mappings. It asserts that the target carries out, hosts or realises the source. It is not a claim that a requirement is met (that is «satisfy», from design element to requirement) and it is not evidence (that is «verify», from test case to requirement).

Allocating DriveMotor to MotorDriver is a sensible action-level allocation. Allocating the whole DeliverDose activity to both MotorDriver and PumpController is a conflict: each dependency asserts that one block performs the entire activity, including steps it has no interface to and no state for, and responsibility is duplicated rather than divided. It is syntactically legal, which is why it survived, but every downstream report is now ambiguous.

Two repairs. (a) Allocate actions, not the frame: DriveMotor to MotorDriver, ReadPressure to the sensor interface block, dose computation to PumpController, leaving DeliverDose as a reusable specification of the procedure. (b) If the two blocks genuinely cooperate, allocate the activity to the block that owns both as parts and allocate sub-actions downward from there.

Two checks before signing off: the allocation should cross a real abstraction boundary (specification to design), and each target block should own the allocated behaviour, as classifier behaviour or an owned property, otherwise the allocation is an annotation nothing can verify. Attach a «rationale» comment to each allocation explaining why that block, since allocation matrices without rationale decay fastest.