FCSTM Simulation Guide
This guide introduces how to simulate FCSTM state machines in Python. The simulation runtime provides an interactive execution environment for testing, prototyping, and understanding state machine behavior before code generation.
Core Concepts
Before diving into usage, understand these key concepts:
State Types
Leaf State: A state with no children (can execute
duringactions)Composite State: A state containing child states (requires initial transitions)
Pseudo State: A special leaf state that skips ancestor aspect actions
Stoppable State: A leaf state (non-pseudo) where a cycle can end
Lifecycle Actions
enter: Executed when entering a state
during: Executed while remaining in a state (each cycle)
exit: Executed when leaving a state
Aspect Actions
>> during before/after: Cross-cutting actions that apply to all descendant leaf states
Pseudo states skip ancestor aspect actions
Composite State Actions
during before (without
>>): Executed when entering composite state from parent ([*] -> Child)during after (without
>>): Executed when exiting composite state to parent (Child -> [*])NOT executed during child-to-child transitions (
Child1 -> Child2)
Python Usage
Creating and Running Simulations
The basic workflow:
Parse DSL code into an AST
Convert AST to a state machine model
Create a
SimulationRuntimeinstanceExecute cycles with
runtime.cycle()
#!/usr/bin/env python3
from pyfcstm.dsl import parse_with_grammar_entry
from pyfcstm.model import parse_dsl_node_to_state_machine
from pyfcstm.simulate import SimulationRuntime
dsl_code = """
def int counter = 0;
state System {
[*] -> Idle;
state Idle {
during {
counter = counter + 1;
}
}
state Active {
during {
counter = counter + 10;
}
}
Idle -> Active : if [counter >= 5];
Active -> Idle : if [counter >= 50];
}
"""
# Parse and create state machine
ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl')
sm = parse_dsl_node_to_state_machine(ast)
# Create runtime
runtime = SimulationRuntime(sm)
# Execute cycles
print(f"Initial: state={'.'.join(runtime.current_state.path)}, counter={runtime.vars['counter']}")
for i in range(1, 8):
runtime.cycle()
print(f"Cycle {i}: state={'.'.join(runtime.current_state.path)}, counter={runtime.vars['counter']}")
Output:
Initial: state=System, counter=0
Cycle 1: state=System.Idle, counter=1
Cycle 2: state=System.Idle, counter=2
Cycle 3: state=System.Idle, counter=3
Cycle 4: state=System.Idle, counter=4
Cycle 5: state=System.Idle, counter=5
Cycle 6: state=System.Active, counter=15
Cycle 7: state=System.Active, counter=25
Key APIs:
runtime.cycle(): Execute one complete cycleruntime.current_state: Get current state object (use.pathfor tuple or'.'.join(.path)for string)runtime.vars: Access/modify variables as a dictionaryruntime.is_terminated: Check if state machine has terminated
Triggering Events
Pass event names to cycle() to trigger transitions:
#!/usr/bin/env python3
from pyfcstm.dsl import parse_with_grammar_entry
from pyfcstm.model import parse_dsl_node_to_state_machine
from pyfcstm.simulate import SimulationRuntime
dsl_code = """
def int counter = 0;
state System {
[*] -> Idle;
state Idle;
state Active;
Idle -> Active :: Start;
Active -> Idle :: Stop;
}
"""
# Parse and create state machine
ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl')
sm = parse_dsl_node_to_state_machine(ast)
# Create runtime
runtime = SimulationRuntime(sm)
# Initialize
runtime.cycle()
print(f"Initial: state={'.'.join(runtime.current_state.path)}")
# Trigger Start event
runtime.cycle(['Start'])
print(f"After 'Start': state={'.'.join(runtime.current_state.path)}")
# Trigger Stop event
runtime.cycle(['Stop'])
print(f"After 'Stop': state={'.'.join(runtime.current_state.path)}")
# Try Start again
runtime.cycle(['Start'])
print(f"After 'Start' again: state={'.'.join(runtime.current_state.path)}")
Output:
Initial: state=System.Idle
After 'Start': state=System.Active
After 'Stop': state=System.Idle
After 'Start' again: state=System.Active
Event Scoping:
::creates local events (scoped to source state):creates chain events (scoped to parent state)/creates absolute events (scoped to root state)
Hot Start from Specific State
Use the initial_state and initial_vars parameters to start execution from an arbitrary state without executing enter actions:
# Hot start from Active state with custom variable values
runtime = SimulationRuntime(
sm,
initial_state="System.Active",
initial_vars={"counter": 100, "flag": 1}
)
# First cycle starts from Active state (no enter actions executed)
runtime.cycle()
print(f"State: {'.'.join(runtime.current_state.path)}")
print(f"Counter: {runtime.vars['counter']}") # 110 (100 + 10)
Key Points:
initial_stateaccepts string path ("System.Active"), tuple path (('System', 'Active')), or State objectinitial_varsmust provide all variables (partial override not supported)Enter actions are skipped for all states in the path
During actions execute normally starting from the first cycle
For composite states, the runtime automatically performs initial transitions to find a stoppable leaf state
Implementing Abstract Handlers
Use the @abstract_handler decorator to implement custom logic:
#!/usr/bin/env python3
from pyfcstm.dsl import parse_with_grammar_entry
from pyfcstm.model import parse_dsl_node_to_state_machine
from pyfcstm.simulate import SimulationRuntime, abstract_handler
dsl_code = """
def int counter = 0;
state System {
[*] -> Active;
state Active {
enter abstract Init;
during abstract Monitor;
exit abstract Cleanup;
during {
counter = counter + 1;
}
}
state Done;
Active -> Done : if [counter >= 5];
}
"""
# Define handler class
class MyHandlers:
@abstract_handler('System.Active.Init')
def handle_init(self, ctx):
print(f"[Init] state={ctx.get_full_state_path()}, counter={ctx.get_var('counter')}")
@abstract_handler('System.Active.Monitor')
def handle_monitor(self, ctx):
print(f"[Monitor] counter={ctx.get_var('counter')}")
@abstract_handler('System.Active.Cleanup')
def handle_cleanup(self, ctx):
print(f"[Cleanup] final_counter={ctx.get_var('counter')}")
# Parse and create state machine
ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl')
sm = parse_dsl_node_to_state_machine(ast)
# Create runtime and register handlers
runtime = SimulationRuntime(sm)
handlers = MyHandlers()
runtime.register_handlers_from_object(handlers)
# Execute cycles
for i in range(1, 7):
runtime.cycle()
print(f"Cycle {i}: state={'.'.join(runtime.current_state.path)}")
Output:
[Init] state=System.Active, counter=0
[Monitor] counter=0
Cycle 1: state=System.Active
[Monitor] counter=1
Cycle 2: state=System.Active
[Monitor] counter=2
Cycle 3: state=System.Active
[Monitor] counter=3
Cycle 4: state=System.Active
[Monitor] counter=4
Cycle 5: state=System.Active
[Cleanup] final_counter=5
Cycle 6: state=System.Done
Handler Context API:
@abstract_handler('System.Active.Monitor')
def handle_monitor(self, ctx):
# Get current state path
state_path = ctx.get_full_state_path()
# Access/modify variables
counter = ctx.get_var('counter')
ctx.set_var('counter', counter + 1)
# Get state object
state = ctx.get_state()
# Access runtime
runtime = ctx.get_runtime()
CLI Usage
The pyfcstm simulate command provides an interactive REPL for testing state machines without writing Python code.
Starting the Simulator
Launch the simulator with a DSL file:
pyfcstm simulate -i example.fcstm
The same command shape works for multi-file import projects. The input is still just the entry file:
pyfcstm simulate -i ./docs/source/tutorials/dsl/import_host_directory.fcstm
This starts an interactive session with command history, auto-completion, and syntax highlighting.
Available Commands
Command |
Description |
|---|---|
|
Execute one or more cycles with optional events. Examples: |
|
Hot start from specific state with variable values. Examples: |
|
Show current state and all variables |
|
List available events in current state |
|
Show execution history (default: 10 recent entries). Use |
|
View or change settings. Without arguments, shows all settings |
|
Export history to file. Supported formats: CSV, JSON, YAML, JSONL (auto-detected from extension) |
|
Show help message with command list |
|
Exit the simulator |
Variable Value Formats for init command:
Decimal integers:
counter=10Hexadecimal:
flags=0xFF(255)Binary:
mask=0b1010(10)Floating point:
temp=25.5Scientific notation:
value=1.5e2(150.0)
Interactive Features
Tab completion: Press Tab to complete commands, events, and settings
History search: Press Ctrl+R to search command history
Auto-suggestions: Previous commands appear as gray suggestions
Color output: Syntax highlighting for states, variables, and events
Example Session
$ pyfcstm simulate -i example.fcstm
╔══════════════════════════════════════════════════════════╗
║ State Machine Interactive Simulator ║
╟──────────────────────────────────────────────────────────╢
║ Type 'help' to see available commands ║
╚══════════════════════════════════════════════════════════╝
simulate> current
Cycle: 0
Current State: System.Idle
Variables:
counter = 0
temperature = 25.0
simulate> events
Available Events:
• Start (System.Events.Start)
• Reset (System.Events.Reset)
simulate> cycle Start
Cycle: 1
Current State: System.Running.Active
Variables:
counter = 1
temperature = 25.1
simulate> cycle 5
Cycle State counter temperature
--------------------------------------------
2 Root.Active 2 25.2
3 Root.Active 3 25.3
4 Root.Active 4 25.4
5 Root.Active 5 25.5
6 Root.Active 6 25.6
simulate> history 3
Cycle State counter temperature
--------------------------------------------
4 Root.Active 4 25.4
5 Root.Active 5 25.5
6 Root.Active 6 25.6
simulate> export history.csv
Exported 6 history entries to history.csv
simulate> quit
Goodbye!
Batch Mode
Execute commands non-interactively using the -e flag:
pyfcstm simulate -i example.fcstm -e "current; cycle Start; current; events"
Output:
────────────────────────────────────────────────────────────
>>> current
────────────────────────────────────────────────────────────
Current State: System.Idle
Variables:
counter = 0
temperature = 25.0
────────────────────────────────────────────────────────────
>>> cycle Start
────────────────────────────────────────────────────────────
Current State: System.Running.Active
Variables:
counter = 1
temperature = 25.1
────────────────────────────────────────────────────────────
>>> events
────────────────────────────────────────────────────────────
Available Events:
• Stop (System.Events.Stop)
• Pause (System.Events.Pause)
Batch mode is useful for automated testing, CI/CD pipelines, and scripting.
Configuration Settings
Setting |
Default |
Description |
|---|---|---|
|
20 |
Maximum rows displayed in tables |
|
100 |
Maximum history entries to keep |
|
on |
Enable/disable color output (on/off) |
|
info |
Logging verbosity (debug/info/warning/error/off) |
Example:
simulate> setting
Current Settings:
table_max_rows = 20
history_size = 100
color = on
log_level = info
simulate> setting log_level debug
Setting 'log_level' set to: debug
Export Formats
Format |
Description |
|---|---|
CSV |
Semicolon-separated values with headers ( |
JSON |
JSON array with objects containing |
YAML |
YAML array with the same structure as JSON |
JSONL |
JSON Lines format (one JSON object per line) |
Example:
simulate> export history.csv
Exported 6 history entries to history.csv
Command Line Options
Option |
Description |
|---|---|
|
State machine DSL file path (required) |
|
Execute batch commands (semicolon-separated) and exit |
|
Disable color output |
Execution Semantics
Understanding how state machines execute is crucial for building correct behavior. This section provides detailed examples with step-by-step execution traces.
Cycle Execution
A cycle executes until reaching a stable boundary:
Follows transition chains until reaching a stoppable state (leaf state, non-pseudo)
Executes the
duringaction at the final stoppable stateMay execute multiple transitions in one cycle (e.g., through pseudo states)
If no transition fires, executes the current state’s
duringaction
Example 1: Basic Transition
def int counter = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
during {
counter = counter + 10;
}
}
[*] -> A;
A -> B :: Go;
}
State machine diagram
Execution Summary:
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
(none) |
Root.A |
2 |
No event, stay in A, execute |
3 |
|
Root.B |
12 |
Event |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0Execute initial transition
[*] -> AExecute
A.enter(none defined)Reach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Result:
state = Root.A,counter = 1
Cycle 2 (no event):
Current state:
Root.A,counter = 1Check transitions:
A -> B :: Go(requires event, not triggered)No transition fires
Execute
A.during:counter = 1 + 1 = 2Result:
state = Root.A,counter = 2
Cycle 3 (with event Go):
Current state:
Root.A,counter = 2Check transitions:
A -> B :: Go(event matches!)Execute
A.exit(none defined)Execute transition (no effect)
Execute
B.enter(none defined)Reach stoppable state
BExecute
B.during:counter = 2 + 10 = 12Result:
state = Root.B,counter = 12
Example 2: Composite State with Initial Transition
def int counter = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
state B1 {
during {
counter = counter + 10;
}
}
state B2 {
during {
counter = counter + 100;
}
}
[*] -> B1;
B1 -> B2 :: Next;
}
[*] -> A;
A -> B :: GoB;
}
Composite state diagram
Execution Summary:
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
|
Root.B.B1 |
11 |
Event |
3 |
|
Root.B.B2 |
111 |
Event |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Result:
state = Root.A,counter = 1
Cycle 2 (with event GoB):
Current state:
Root.A,counter = 1Check transitions:
A -> B :: GoB(event matches!)Execute
A.exit(none defined)Execute
B.enter(none defined)B is composite state - must follow initial transition
Execute
[*] -> B1(inside B)Execute
B1.enter(none defined)Reach stoppable state
B1Execute
B1.during:counter = 1 + 10 = 11Result:
state = Root.B.B1,counter = 11
Key Point: When transitioning to a composite state, the cycle continues by following initial transitions until reaching a stoppable state.
Cycle 3 (with event Next):
Current state:
Root.B.B1,counter = 11Check transitions:
B1 -> B2 :: Next(event matches!)Execute
B1.exit(none defined)Execute
B2.enter(none defined)Reach stoppable state
B2Execute
B2.during:counter = 11 + 100 = 111Result:
state = Root.B.B2,counter = 111
Example 3: Aspect Actions
def int trace = 0;
state Root {
>> during before {
trace = trace * 10 + 1;
}
>> during after {
trace = trace * 10 + 3;
}
state A {
during {
trace = trace * 10 + 2;
}
}
[*] -> A;
}
Aspect actions diagram
Execution Summary:
Cycle |
Event |
State |
trace |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
123 |
Initial transition |
2 |
(none) |
Root.A |
123123 |
No event, execute: before (×10+1=1231) → during (×10+2=12312) → after (×10+3=123123) |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
trace = 0Execute
[*] -> AReach stoppable state
AExecute during phase: 1.
Root >> during before:trace = 0 * 10 + 1 = 12.A.during:trace = 1 * 10 + 2 = 123.Root >> during after:trace = 12 * 10 + 3 = 123Result:
state = Root.A,trace = 123
Cycle 2 (no event):
Current state:
Root.A,trace = 123No transition fires
Execute during phase: 1.
Root >> during before:trace = 123 * 10 + 1 = 12312.A.during:trace = 1231 * 10 + 2 = 123123.Root >> during after:trace = 12312 * 10 + 3 = 123123Result:
state = Root.A,trace = 123123
Key Point: Aspect actions (>> during before/after) execute in hierarchical order around the leaf state’s during action, creating a sandwich pattern: before → during → after.
Example 4: Pseudo State (Skipping Aspect Actions)
def int trace = 0;
state Root {
>> during before {
trace = trace * 10 + 1;
}
>> during after {
trace = trace * 10 + 3;
}
pseudo state A {
during {
trace = trace * 10 + 2;
}
}
[*] -> A;
A -> [*] : if [trace >= 2];
}
Pseudo state diagram
Execution Summary:
Cycle |
Event |
State |
trace |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
(terminated) |
2 |
Initial transition |
Detailed Execution Trace:
Cycle 1 (initialization and termination):
Initial state:
trace = 0Execute
[*] -> AReach stoppable state
A(pseudo state)Pseudo state skips aspect actions!
Execute during phase: -
Root >> during beforeSKIPPED -A.during:trace = 0 * 10 + 2 = 2-Root >> during afterSKIPPEDCheck transitions:
A -> [*] : if [trace >= 2](guard satisfied!)Execute
A.exit(none defined)Transition to final state
Result:
state = terminated,trace = 2
Key Point: Pseudo states skip all ancestor aspect actions, executing only their own during action. This is useful for intermediate states that shouldn’t trigger cross-cutting concerns.
Example 5: Multi-Level Composite State
def int counter = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
state B1 {
state B1a {
during {
counter = counter + 10;
}
}
[*] -> B1a;
}
[*] -> B1;
}
state C {
during {
counter = counter + 100;
}
}
[*] -> A;
A -> B :: GoB;
A -> C :: GoC;
}
Multi-level composite state diagram
Execution Summary (Scenario 1: A → B):
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
|
Root.B.B1.B1a |
11 |
Event |
Execution Summary (Scenario 2: A → C):
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
|
Root.C |
101 |
Event |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Result:
state = Root.A,counter = 1
Cycle 2 (with event GoB):
Current state:
Root.A,counter = 1Check transitions:
A -> B :: GoB(event matches!)Execute
A.exit(none defined)Execute
B.enter(none defined)B is composite - follow
[*] -> B1Execute
B1.enter(none defined)B1 is also composite - follow
[*] -> B1aExecute
B1a.enter(none defined)Reach stoppable state
B1aExecute
B1a.during:counter = 1 + 10 = 11Result:
state = Root.B.B1.B1a,counter = 11
Key Point: A single cycle can traverse multiple levels of composite states by following initial transition chains until reaching a stoppable leaf state.
Cycle 3 (with event GoC from initial state):
Starting fresh:
counter = 0Execute
[*] -> AExecute
A.during:counter = 1Next cycle with event
GoC:Check transitions:
A -> C :: GoC(event matches!)Execute
A.exit(none defined)Execute
C.enter(none defined)Reach stoppable state
CExecute
C.during:counter = 1 + 100 = 101Result:
state = Root.C,counter = 101
Hierarchical Execution Order
Understanding execution order in nested states is crucial:
#!/usr/bin/env python3
from pyfcstm.dsl import parse_with_grammar_entry
from pyfcstm.model import parse_dsl_node_to_state_machine
from pyfcstm.simulate import SimulationRuntime
dsl_code = """
def int counter = 0;
state System {
>> during before {
counter = counter + 1;
}
[*] -> Parent;
state Parent {
during before {
counter = counter + 100;
}
state Child {
during {
counter = counter + 10;
}
}
[*] -> Child;
}
}
"""
# Parse and create state machine
ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl')
sm = parse_dsl_node_to_state_machine(ast)
# Create runtime
runtime = SimulationRuntime(sm)
# Execute first cycle (initialization)
runtime.cycle()
print(f"Cycle 1: state={'.'.join(runtime.current_state.path)}, counter={runtime.vars['counter']}")
print(f" → Parent.during before (100) executed during entry")
# Execute second cycle (during phase)
runtime.cycle()
print(f"\nCycle 2: state={'.'.join(runtime.current_state.path)}, counter={runtime.vars['counter']}")
print(f" → >> during before (1) + Child.during (10)")
# Execute third cycle
runtime.cycle()
print(f"\nCycle 3: state={'.'.join(runtime.current_state.path)}, counter={runtime.vars['counter']}")
print(f" → >> during before (1) + Child.during (10)")
Output:
Cycle 1: state=System.Parent.Child, counter=111
→ Parent.during before (100) executed during entry
Cycle 2: state=System.Parent.Child, counter=122
→ >> during before (1) + Child.during (10)
Cycle 3: state=System.Parent.Child, counter=133
→ >> during before (1) + Child.during (10)
Complete Execution Order:
Entry Phase (from parent):
State.enterState.during before(if entering via[*] -> Child)Child.enter
During Phase (each cycle at leaf state):
Ancestor
>> during beforeactions (root to leaf)Leaf state
duringactionAncestor
>> during afteractions (leaf to root)
Exit Phase (to parent):
Child.exitState.during after(if exiting viaChild -> [*])State.exit
Child-to-Child Transition:
Child1.exit(Transition effect)
Child2.enterNO
during before/afterexecution
Key Points:
Aspect actions (
>> during before/after) execute during theduringphase for all descendant leaf statesComposite state actions (
during before/afterwithout>>) only execute during entry/exit transitions, NOT during theduringphasePseudo states skip ancestor aspect actions
Example 6: Transition Priority
When multiple transitions from the same state have satisfied guards, the first transition in definition order is selected:
def int counter = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
during {
counter = counter + 10;
}
}
state C {
during {
counter = counter + 100;
}
}
[*] -> A;
A -> B : if [counter >= 3];
A -> C : if [counter >= 3];
}
Transition priority diagram
Execution Summary:
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
(none) |
Root.A |
2 |
No guard satisfied (counter < 3), execute |
3 |
(none) |
Root.A |
3 |
No guard satisfied (counter < 3), execute |
4 |
(none) |
Root.B |
13 |
Both guards satisfied (counter >= 3), but |
Detailed Execution Trace:
Cycle 1-3 (accumulating counter):
Initial state:
counter = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Cycles 2-3 continue incrementing:
counter = 2, thencounter = 3
Cycle 4 (transition priority):
Current state:
Root.A,counter = 3Check transitions in definition order: 1.
A -> B : if [counter >= 3](guard satisfied!) 2.A -> C : if [counter >= 3](guard also satisfied, but not checked)Execute
A.exit(none defined)Execute
B.enter(none defined)Reach stoppable state
BExecute
B.during:counter = 3 + 10 = 13Result:
state = Root.B,counter = 13
Key Point: Transitions are evaluated in definition order. The first transition with a satisfied guard is selected, even if multiple guards are satisfied.
Example 7: Self-Transition
Self-transitions execute exit and enter actions, providing a way to reset state-specific initialization:
def int counter = 0;
state Root {
state A {
enter {
counter = counter + 1;
}
during {
counter = counter + 10;
}
exit {
counter = counter + 100;
}
}
[*] -> A;
A -> A :: Loop;
}
Self-transition diagram
Execution Summary:
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
11 |
Initial transition |
2 |
(none) |
Root.A |
21 |
No event, stay in A, execute |
3 |
(none) |
Root.A |
31 |
No event, stay in A, execute |
4 |
|
Root.A |
142 |
Event |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0Execute
[*] -> AExecute
A.enter:counter = 0 + 1 = 1Reach stoppable state
AExecute
A.during:counter = 1 + 10 = 11Result:
state = Root.A,counter = 11
Cycle 2-3 (staying in state without transition):
Current state:
Root.A,counter = 11No event provided, no transition fires
Stay in state
AExecute
A.during:counter = 11 + 10 = 21Result:
state = Root.A,counter = 21Cycle 3: Same process,
counter = 21 + 10 = 31
Cycle 4 (self-transition with event Loop):
Current state:
Root.A,counter = 31Check transitions:
A -> A :: Loop(event matches!)Execute
A.exit:counter = 31 + 100 = 131Execute transition (no effect)
Execute
A.enter:counter = 131 + 1 = 132Reach stoppable state
A(same state)Execute
A.during:counter = 132 + 10 = 142Result:
state = Root.A,counter = 142
Key Point: Self-transitions (A -> A) execute the full exit-enter sequence, allowing state reinitialization. This is different from staying in the state without a transition:
Staying in state (cycles 2-3): Only
duringaction executes (+10 each cycle)Self-transition (cycle 4): Full sequence executes:
exit(+100) →enter(+1) →during(+10)
Example 8: Guard Conditions with Effects
Guards and effects work together to enable complex conditional transitions with state modifications:
def int counter = 0;
def int flag = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
enter {
flag = 1;
}
state B1 {
during {
counter = counter + 10;
}
}
[*] -> B1 : if [flag == 1];
}
[*] -> A;
A -> B : if [counter >= 3] effect {
flag = 1;
};
}
Guard and effect diagram
Execution Summary:
Cycle |
Event |
State |
counter |
flag |
Reason |
|---|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
0 |
Initial transition |
2 |
(none) |
Root.A |
2 |
0 |
Guard not satisfied (counter < 3), execute |
3 |
(none) |
Root.A |
3 |
0 |
Guard not satisfied (counter < 3), execute |
4 |
(none) |
Root.B.B1 |
13 |
1 |
Guard satisfied (counter >= 3), effect sets flag=1, |
Detailed Execution Trace:
Cycle 1-3 (accumulating counter):
Initial state:
counter = 0,flag = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Cycles 2-3 continue:
counter = 2, thencounter = 3
Cycle 4 (guard satisfied, effect executed):
Current state:
Root.A,counter = 3,flag = 0Check transitions:
A -> B : if [counter >= 3](guard satisfied!)Execute
A.exit(none defined)Execute transition effect:
flag = 1Execute
B.enter:flag = 1(enter action sets flag)B is composite - follow
[*] -> B1 : if [flag == 1](guard satisfied!)Execute
B1.enter(none defined)Reach stoppable state
B1Execute
B1.during:counter = 3 + 10 = 13Result:
state = Root.B.B1,counter = 13,flag = 1
Key Point: Transition effects execute after exit actions but before enter actions. The effect can modify variables that are checked by guards in subsequent initial transitions, enabling complex multi-stage validation.
DFS Validation Mechanism
When transitioning to a non-stoppable state (composite or pseudo), the runtime performs a depth-first search (DFS) to validate that a stoppable state can be reached. This prevents the state machine from entering invalid states.
Validation Rules:
Composite States: Must have at least one initial transition that leads to a stoppable state
Pseudo States: Must have an outgoing transition that leads to a stoppable state
Event Requirements: All required events must be available in the current cycle
Guard Conditions: All guards along the path must be satisfied
Transition Order: Transitions are evaluated in definition order (DFS, not BFS)
Validation Process:
Create a snapshot of current variables
Simulate the transition chain using DFS: - Execute enter actions (modifying snapshot) - Check guards (using snapshot) - Follow initial transitions recursively
If a stoppable state is reached: validation succeeds, execute the real transition
If no stoppable state is reachable: validation fails, stay in current state
Example 9: Pseudo State Chain Validation
Pseudo states require validation to ensure they lead to stoppable states:
def int counter = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
pseudo state P {
enter {
counter = counter + 10;
}
during {
counter = counter + 100;
}
}
state B {
during {
counter = counter + 1000;
}
}
[*] -> A;
A -> P :: GoP;
P -> B :: GoB;
}
Pseudo state chain diagram
Execution Summary:
Cycle |
Event |
State |
counter |
Reason |
|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
Initial transition |
2 |
|
Root.A |
2 |
Event |
3 |
|
Root.B |
1112 |
Both events provided, validation succeeds: |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Result:
state = Root.A,counter = 1
Cycle 2 (validation failure - missing event):
Current state:
Root.A,counter = 1Check transitions:
A -> P :: GoP(event matches!)Validation phase (using snapshot): - Target
Pis pseudo state (non-stoppable) - Simulate: executeP.enter:counter_snapshot = 1 + 10 = 11- CheckP’s transitions:P -> B :: GoB(requiresGoBevent) - EventGoBNOT available in current cycle - Validation fails: cannot reach stoppable stateTransition rejected, stay in
AExecute
A.during:counter = 1 + 1 = 2Result:
state = Root.A,counter = 2
Cycle 3 (validation success - all events provided):
Current state:
Root.A,counter = 2Check transitions:
A -> P :: GoP(event matches!)Validation phase (using snapshot): - Target
Pis pseudo state (non-stoppable) - Simulate: executeP.enter:counter_snapshot = 2 + 10 = 12- CheckP’s transitions:P -> B :: GoB(requiresGoBevent) - EventGoBIS available in current cycle - Simulate: executeB.enter(none defined) - TargetBis stoppable state - Validation succeeds: can reach stoppable stateBReal execution: - Execute
A.exit(none defined) - ExecuteP.enter:counter = 2 + 10 = 12- Reach pseudo stateP(non-stoppable, continue immediately) - ExecuteP.during:counter = 12 + 100 = 112- ExecuteP.exit(none defined) - ExecuteB.enter(none defined) - Reach stoppable stateB- ExecuteB.during:counter = 112 + 1000 = 1112Result:
state = Root.B,counter = 1112
Key Point: Pseudo states are non-stoppable and require validation. The validation uses DFS to check if the transition chain can reach a stoppable state with the available events. Pseudo states execute their during action during the real transition, but this happens in the same cycle as reaching the final stoppable state.
Example 10: Validation Failure - Unreachable Stoppable
When a composite state’s initial transitions cannot reach a stoppable state, the transition is rejected:
def int counter = 0;
def int ready = 0;
state Root {
state A {
during {
counter = counter + 1;
}
}
state B {
state B1;
[*] -> B1 : if [ready == 1];
}
[*] -> A;
A -> B :: GoB;
}
Validation failure diagram
Execution Summary:
Cycle |
Event |
State |
counter |
ready |
Reason |
|---|---|---|---|---|---|
0 |
(none) |
(initial) |
0 |
0 |
Initial variable values |
1 |
(none) |
Root.A |
1 |
0 |
Initial transition |
2 |
|
Root.A |
2 |
0 |
Event |
Detailed Execution Trace:
Cycle 1 (initialization):
Initial state:
counter = 0,ready = 0Execute
[*] -> AReach stoppable state
AExecute
A.during:counter = 0 + 1 = 1Result:
state = Root.A,counter = 1,ready = 0
Cycle 2 (validation failure - guard not satisfied):
Current state:
Root.A,counter = 1,ready = 0Check transitions:
A -> B :: GoB(event matches!)Validation phase (using snapshot): - Target
Bis composite state (non-stoppable) - Simulate: executeB.enter(none defined) - CheckB’s initial transitions:[*] -> B1 : if [ready == 1]- Guard check:ready == 1(current value:ready = 0) - Guard NOT satisfied - No other initial transitions available - Validation fails: cannot reach stoppable stateTransition rejected, stay in
AExecute
A.during:counter = 1 + 1 = 2Result:
state = Root.A,counter = 2,ready = 0
Key Point: Composite states must have at least one initial transition that can reach a stoppable state. The validation checks all guards and event requirements along the path. If no valid path exists, the transition is rejected and the state machine remains in the current state.
Hot Start Feature
The hot start feature allows starting execution from an arbitrary state without executing enter actions. This section demonstrates the mechanism through concrete examples showing how hot start constructs the execution stack and affects lifecycle action execution.
Example 14: Hot Start from Leaf State
This example demonstrates hot starting from a leaf state, showing how the runtime skips enter actions but executes during actions normally.
def int counter = 0;
def int enter_count = 0;
state System {
state Idle {
enter { enter_count = enter_count + 1; }
during { counter = counter + 1; }
}
state Active {
enter { enter_count = enter_count + 1; }
during { counter = counter + 10; }
}
[*] -> Idle;
}
Scenario A: Normal Initialization (for comparison)
Cycle |
State |
counter |
enter_count |
Execution Details |
|---|---|---|---|---|
0 |
System |
0 |
0 |
Initial state, before first cycle |
1 |
System.Idle |
1 |
1 |
Enter: |
2 |
System.Idle |
2 |
1 |
During: |
Scenario B: Hot Start from Active State
Hot start configuration: initial_state="System.Active", initial_vars={"counter": 100, "enter_count": 0}
Cycle |
State |
counter |
enter_count |
Execution Details |
|---|---|---|---|---|
0 |
System.Active |
100 |
0 |
Hot start: Stack constructed directly to Active. No enter actions executed |
1 |
System.Active |
110 |
0 |
During: |
2 |
System.Active |
120 |
0 |
During: |
Key Observations:
Hot start skips
Active.enteraction (enter_countstays 0)Active.duringexecutes normally from first cycleStack is constructed with
Activein'active'modeBehaves as if already entered and stabilized at Active state
Example 15: Hot Start with Aspect Actions
This example shows how aspect actions (>> during before/after) execute normally during hot start, while enter actions are skipped.
def int counter = 0;
def int aspect_count = 0;
def int enter_count = 0;
state Root {
>> during before {
aspect_count = aspect_count + 1;
}
state A {
enter { enter_count = enter_count + 1; }
during { counter = counter + 1; }
}
state B {
enter { enter_count = enter_count + 1; }
during { counter = counter + 10; }
}
[*] -> A;
}
Scenario: Hot Start from State B
Hot start configuration: initial_state="Root.B", initial_vars={"counter": 50, "aspect_count": 0, "enter_count": 0}
Cycle |
State |
counter |
aspect_count |
enter_count |
Execution Details |
|---|---|---|---|---|---|
0 |
Root.B |
50 |
0 |
0 |
Hot start: Stack = [Root(active), B(active)]. Enter actions skipped |
1 |
Root.B |
60 |
1 |
0 |
Aspect before: |
2 |
Root.B |
70 |
2 |
0 |
Aspect before: |
Key Observations:
Enter actions skipped (
enter_count= 0)Aspect actions (
>> during before) execute normallyDuring actions execute normally
Aspect actions apply to all descendant leaf states, including hot-started states
Example 16: Hot Start from Composite State
This example demonstrates hot starting from a composite state, showing how the runtime automatically performs initial transitions to find a stoppable leaf state.
def int counter = 0;
def int ready = 1;
state System {
state SubSystem {
state Idle {
during { counter = counter + 1; }
}
state Ready {
during { counter = counter + 10; }
}
[*] -> Idle : if [ready == 0];
[*] -> Ready : if [ready == 1];
}
[*] -> SubSystem;
}
Scenario A: Hot Start with ready=1
Hot start configuration: initial_state="System.SubSystem", initial_vars={"counter": 0, "ready": 1}
Cycle |
State |
counter |
ready |
Execution Details |
|---|---|---|---|---|
0 |
System.SubSystem |
0 |
1 |
Hot start: Stack = [System(active), SubSystem(init_wait)] |
1 |
System.SubSystem.Ready |
10 |
1 |
DFS triggered: Check |
2 |
System.SubSystem.Ready |
20 |
1 |
During: |
Scenario B: Hot Start with ready=0
Hot start configuration: initial_state="System.SubSystem", initial_vars={"counter": 0, "ready": 0}
Cycle |
State |
counter |
ready |
Execution Details |
|---|---|---|---|---|
0 |
System.SubSystem |
0 |
0 |
Hot start: Stack = [System(active), SubSystem(init_wait)] |
1 |
System.SubSystem.Idle |
1 |
0 |
DFS triggered: Check |
2 |
System.SubSystem.Idle |
2 |
0 |
During: |
Key Observations:
Composite state hot start uses
'init_wait'modeFirst cycle triggers DFS to find initial transition
Guard conditions evaluated to select correct path
Automatically navigates to stoppable leaf state
If no valid initial transition exists, validation fails
Stack Construction Summary:
Target State Type |
Frame Mode |
Behavior |
|---|---|---|
Leaf state (target) |
|
First cycle executes during chain (including aspect actions) |
Composite state (target) |
|
First cycle triggers DFS for initial transition |
Ancestor states (path) |
|
Represent child states running, aspect actions execute |
Real-World Business Examples
The following examples demonstrate practical applications of FCSTM state machines in real-world control systems. Each example includes multiple execution scenarios showing different business conditions and their detailed execution traces.
Example 11: Elevator Door Control
This example simulates common elevator car door control logic: doors open when a hall call is received, remain open for a hold period, then automatically close. If the infrared beam detects an obstruction during closing, the doors immediately reopen and restart the hold timer.
def int door_pos = 0;
def int hold = 0;
def int reopen_count = 0;
state Root {
state Closed {
during {
hold = 0;
}
}
state Opening {
during {
door_pos = door_pos + 50;
}
}
state Opened {
during {
hold = hold + 1;
}
}
state Closing {
during {
door_pos = door_pos - 50;
}
}
[*] -> Closed;
Closed -> Opening :: HallCall effect {
hold = 0;
};
Opening -> Opened : if [door_pos >= 100] effect {
hold = 0;
};
Opened -> Closing : if [hold >= 2];
Closing -> Opened :: BeamBlocked effect {
reopen_count = reopen_count + 1;
door_pos = 100;
hold = 0;
};
Closing -> Closed : if [door_pos <= 0] effect {
hold = 0;
};
}
Elevator door control diagram
Business Context:
This state machine models a typical elevator safety system where:
door_posrepresents door position (0=fully closed, 50=half-open, 100=fully open)holdcounts cycles the door remains fully openreopen_counttracks how many times the door reopened due to obstructions
Scenario A: Normal Operation (open → hold → close)
Cycle |
Event |
State |
door_pos |
hold |
Business Meaning |
|---|---|---|---|---|---|
1 |
(none) |
Closed |
0 |
0 |
Elevator idle, doors closed |
2 |
|
Opening |
50 |
0 |
Passenger calls elevator, doors begin opening |
3 |
(none) |
Opening |
100 |
0 |
Doors continue opening to full position |
4 |
(none) |
Opened |
100 |
1 |
Doors fully open, hold timer starts |
5 |
(none) |
Opened |
100 |
2 |
Hold timer continues (waiting for passengers) |
6 |
(none) |
Closing |
50 |
2 |
Hold time expired, doors begin closing |
7 |
(none) |
Closing |
0 |
2 |
Doors continue closing to fully closed |
8 |
(none) |
Closed |
0 |
0 |
Doors fully closed, ready for next call |
Detailed Execution Trace A:
Cycle 1 (initial state):
Initial:
door_pos = 0,hold = 0,reopen_count = 0Execute
[*] -> ClosedExecute
Closed.during:hold = 0Result: Elevator idle with doors closed
Cycle 2 (hall call received):
Event
HallCalltriggersClosed -> OpeningExecute
Closed.exit(none defined)Execute transition effect:
hold = 0Execute
Opening.enter(none defined)Execute
Opening.during:door_pos = 0 + 50 = 50Result: Doors begin opening, halfway open
Cycle 3 (doors continue opening):
Check
Opening -> Opened:door_pos >= 100not satisfied (current: 50)Execute
Opening.during:door_pos = 50 + 50 = 100Result: Doors reach fully open position
Cycle 4 (transition to opened state):
Check
Opening -> Opened:door_pos >= 100satisfiedExecute
Opening.exit(none defined)Execute transition effect:
hold = 0Execute
Opened.enter(none defined)Execute
Opened.during:hold = 0 + 1 = 1Result: Doors fully open, hold timer starts
Cycle 5 (hold timer continues):
Check
Opened -> Closing:hold >= 2not satisfied (current: 1)Execute
Opened.during:hold = 1 + 1 = 2Result: Hold timer reaches threshold
Cycle 6 (begin closing):
Check
Opened -> Closing:hold >= 2satisfiedExecute
Opened.exit(none defined)Execute
Closing.enter(none defined)Execute
Closing.during:door_pos = 100 - 50 = 50Result: Doors begin closing
Cycle 7 (continue closing):
Check
Closing -> Closed:door_pos <= 0not satisfied (current: 50)Execute
Closing.during:door_pos = 50 - 50 = 0Result: Doors reach fully closed position
Cycle 8 (transition to closed):
Check
Closing -> Closed:door_pos <= 0satisfiedExecute
Closing.exit(none defined)Execute transition effect:
hold = 0Execute
Closed.enter(none defined)Execute
Closed.during:hold = 0Result: Doors fully closed, system ready
Scenario B: Safety Reopening (obstruction detected during closing)
Cycle |
Event |
State |
door_pos |
reopen_count |
Business Meaning |
|---|---|---|---|---|---|
1-5 |
(same as A) |
(same as A) |
(same) |
0 |
Normal opening and hold sequence |
6 |
(none) |
Closing |
50 |
0 |
Doors begin closing automatically |
7 |
|
Opened |
100 |
1 |
Obstruction detected! Doors immediately reopen for safety |
Detailed Execution Trace B:
Cycles 1-6: Same as Scenario A (doors open, hold, begin closing)
- After cycle 6: state = Closing, door_pos = 50, hold = 2, reopen_count = 0
Cycle 7 (obstruction detected):
Event
BeamBlockedtriggersClosing -> OpenedExecute
Closing.exit(none defined)Execute transition effect: -
reopen_count = 0 + 1 = 1(track safety reopening) -door_pos = 100(immediately set to fully open) -hold = 0(restart hold timer)Execute
Opened.enter(none defined)Execute
Opened.during:hold = 0 + 1 = 1Result: Doors immediately reopen for safety, hold timer restarts
Key Points:
door_posis abstracted to three positions (0, 50, 100) representing closed, half, and fully openBeamBlockedevent only has meaning inClosingstate, matching real elevator safety logicReopening transitions directly to
Opened(notOpening), immediately providing clearancereopen_counttracks safety events for maintenance monitoring
Example 12: Water Heater Temperature Control
This example simulates a common residential storage water heater: water temperature gradually decreases during standby, heating activates when temperature drops below the lower threshold, and deactivates when reaching the upper threshold. Heavy water usage causes rapid temperature drop, triggering earlier heating.
def int water_temp = 55;
def int draw_count = 0;
state Root {
state Standby {
during {
water_temp = water_temp - 1;
}
}
state Heating {
during {
water_temp = water_temp + 4;
}
}
[*] -> Standby;
Standby -> Heating : if [water_temp <= 50];
Standby -> Standby :: HotWaterDraw effect {
water_temp = water_temp - 8;
draw_count = draw_count + 1;
};
Heating -> Standby : if [water_temp >= 60];
Heating -> Heating :: HotWaterDraw effect {
water_temp = water_temp - 8;
draw_count = draw_count + 1;
};
}
Water heater temperature control diagram
Business Context:
This state machine models a typical hysteresis temperature control system where:
water_temprepresents water temperature in degreesdraw_counttracks number of hot water usage eventsTemperature naturally decreases by 1°/cycle in standby
Heating increases temperature by 4°/cycle
Hot water draw causes immediate 8° temperature drop
Scenario A: Natural Heat Loss and Recovery (no water usage)
Cycle |
Event |
State |
water_temp |
Business Meaning |
|---|---|---|---|---|
1 |
(none) |
Standby |
54 |
Normal standby, gradual heat loss |
2 |
(none) |
Standby |
53 |
Continued heat loss through insulation |
3 |
(none) |
Standby |
52 |
Temperature approaching lower threshold |
4 |
(none) |
Standby |
51 |
Temperature nearing heating activation point |
5 |
(none) |
Standby |
50 |
Temperature at lower threshold |
6 |
(none) |
Heating |
54 |
Heating activated, temperature begins rising |
7 |
(none) |
Heating |
58 |
Heating continues toward upper threshold |
Detailed Execution Trace A:
Cycle 1 (initial standby):
Initial:
water_temp = 55,draw_count = 0Execute
[*] -> StandbyExecute
Standby.during:water_temp = 55 - 1 = 54Result: Normal heat loss through tank insulation
Cycles 2-5 (gradual temperature decrease):
Each cycle: Check
Standby -> Heating:water_temp <= 50not satisfiedExecute
Standby.during:water_tempdecreases by 1Cycle 2:
54 - 1 = 53Cycle 3:
53 - 1 = 52Cycle 4:
52 - 1 = 51Cycle 5:
51 - 1 = 50Result: Temperature gradually drops to lower threshold
Cycle 6 (heating activation):
Check
Standby -> Heating:water_temp <= 50satisfiedExecute
Standby.exit(none defined)Execute
Heating.enter(none defined)Execute
Heating.during:water_temp = 50 + 4 = 54Result: Heating element activates, temperature begins rising
Cycle 7 (continued heating):
Check
Heating -> Standby:water_temp >= 60not satisfied (current: 54)Execute
Heating.during:water_temp = 54 + 4 = 58Result: Heating continues toward upper threshold
Scenario B: Heavy Water Usage (morning shower triggers early heating)
Cycle |
Event |
State |
water_temp |
draw_count |
Business Meaning |
|---|---|---|---|---|---|
1 |
(none) |
Standby |
54 |
0 |
Normal standby state |
2 |
|
Standby |
45 |
1 |
Heavy water usage (shower), rapid temperature drop |
3 |
(none) |
Heating |
49 |
1 |
Temperature below threshold, heating activates |
4 |
(none) |
Heating |
53 |
1 |
Heating continues |
5 |
(none) |
Heating |
57 |
1 |
Approaching upper threshold |
6 |
(none) |
Heating |
61 |
1 |
Temperature exceeds upper threshold |
7 |
(none) |
Standby |
60 |
1 |
Heating deactivates, return to standby |
Detailed Execution Trace B:
Cycle 1 (initial standby):
Same as Scenario A:
water_temp = 54,draw_count = 0
Cycle 2 (heavy water usage):
Event
HotWaterDrawtriggersStandby -> Standby(self-transition)Execute
Standby.exit(none defined)Execute transition effect: -
water_temp = 54 - 8 = 46(cold water influx) -draw_count = 0 + 1 = 1(track usage event)Execute
Standby.enter(none defined)Execute
Standby.during:water_temp = 46 - 1 = 45Result: Significant temperature drop from water usage
Cycle 3 (heating activation):
Check
Standby -> Heating:water_temp <= 50satisfied (current: 45)Execute
Standby.exit(none defined)Execute
Heating.enter(none defined)Execute
Heating.during:water_temp = 45 + 4 = 49Result: Low temperature triggers immediate heating
Cycles 4-6 (heating to upper threshold):
Each cycle: Check
Heating -> Standby:water_temp >= 60not satisfiedExecute
Heating.during:water_tempincreases by 4Cycle 4:
49 + 4 = 53Cycle 5:
53 + 4 = 57Cycle 6:
57 + 4 = 61Result: Temperature rises above upper threshold
Cycle 7 (heating deactivation):
Check
Heating -> Standby:water_temp >= 60satisfiedExecute
Heating.exit(none defined)Execute
Standby.enter(none defined)Execute
Standby.during:water_temp = 61 - 1 = 60Result: Heating deactivates, system returns to standby
Key Points:
HotWaterDrawmodels significant temperature drop from water usageStandby -> HeatingandHeating -> Standbyform classic hysteresis control (50°-60° deadband)Self-transition
Standby -> Standbyallows water draw during standbySelf-transition
Heating -> Heatingmodels “heating while drawing” scenariodraw_countenables usage pattern analysis for energy management
Example 13: Traffic Light with Pedestrian Crossing
This example simulates a common urban intersection signal controller: the main road maintains green light by default; when a pedestrian button is pressed, the request is latched; only after the minimum green time is satisfied does the controller enter yellow light and pedestrian crossing phases, then returns to main road green.
def int green_ticks = 0;
def int request_latched = 0;
def int yellow_ticks = 0;
def int walk_ticks = 0;
state Root {
state MainGreen {
during {
green_ticks = green_ticks + 1;
}
}
state PedestrianPhase {
state MainYellow {
during {
yellow_ticks = yellow_ticks + 1;
}
}
state PedWalk {
during {
walk_ticks = walk_ticks + 1;
}
}
[*] -> MainYellow;
MainYellow -> PedWalk : if [yellow_ticks >= 1];
PedWalk -> [*] : if [walk_ticks >= 2];
}
[*] -> MainGreen;
MainGreen -> PedestrianPhase : if [request_latched == 1 && green_ticks >= 3] effect {
request_latched = 0;
yellow_ticks = 0;
walk_ticks = 0;
};
MainGreen -> MainGreen :: PedRequest effect {
request_latched = 1;
};
PedestrianPhase -> MainGreen effect {
green_ticks = 0;
yellow_ticks = 0;
walk_ticks = 0;
};
}
Traffic light control diagram
Business Context:
This state machine models a traffic-responsive signal system where:
green_tickscounts cycles the main road has been greenrequest_latchedstores pedestrian button press (latched, not momentary)yellow_tickscounts yellow light durationwalk_tickscounts pedestrian crossing timePedestrianPhaseis a composite state containing yellow and walk sub-phases
Scenario A: No Pedestrian Request (main road priority maintained)
Cycle |
Event |
State |
green_ticks |
Business Meaning |
|---|---|---|---|---|
1 |
(none) |
MainGreen |
1 |
Main road green light active, no pedestrian request |
2 |
(none) |
MainGreen |
2 |
Continued main road priority |
3 |
(none) |
MainGreen |
3 |
Minimum green time satisfied, but no pedestrian waiting |
4 |
(none) |
MainGreen |
4 |
Main road continues with green (efficient traffic flow) |
Detailed Execution Trace A:
Cycle 1 (initial state):
Initial:
green_ticks = 0,request_latched = 0,yellow_ticks = 0,walk_ticks = 0Execute
[*] -> MainGreenExecute
MainGreen.during:green_ticks = 0 + 1 = 1Result: Main road green light active
Cycles 2-4 (continued main road priority):
Each cycle: Check
MainGreen -> PedestrianPhase:request_latched == 1 && green_ticks >= 3not satisfiedExecute
MainGreen.during:green_ticksincrementsCycle 2:
green_ticks = 2Cycle 3:
green_ticks = 3(minimum green satisfied, but no request)Cycle 4:
green_ticks = 4Result: Main road maintains priority without pedestrian demand
Scenario B: Pedestrian Request with Latching (button pressed early, served after minimum green)
Cycle |
Event |
State |
green_ticks |
request_latched |
Business Meaning |
|---|---|---|---|---|---|
1 |
(none) |
MainGreen |
1 |
0 |
Main road green active |
2 |
|
MainGreen |
2 |
1 |
Pedestrian presses button, request latched |
3 |
(none) |
MainGreen |
3 |
1 |
Minimum green not yet satisfied, main road continues |
4 |
(none) |
MainYellow |
3 |
0 |
Minimum green satisfied, enter pedestrian phase (yellow first) |
5 |
(none) |
PedWalk |
3 |
0 |
Yellow complete, pedestrian crossing begins |
6 |
(none) |
PedWalk |
3 |
0 |
Pedestrian crossing continues |
7 |
(none) |
MainGreen |
1 |
0 |
Pedestrian phase complete, return to main road green |
Detailed Execution Trace B:
Cycle 1 (initial state):
Same as Scenario A:
green_ticks = 1,request_latched = 0
Cycle 2 (pedestrian button pressed):
Event
PedRequesttriggersMainGreen -> MainGreen(self-transition)Check
MainGreen -> PedestrianPhase:request_latched == 1 && green_ticks >= 3not satisfiedExecute
MainGreen.exit(none defined)Execute transition effect:
request_latched = 1(latch the request)Execute
MainGreen.enter(none defined)Execute
MainGreen.during:green_ticks = 1 + 1 = 2Result: Request latched, but minimum green not yet satisfied
Cycle 3 (waiting for minimum green):
Check
MainGreen -> PedestrianPhase:request_latched == 1 && green_ticks >= 3not satisfied (current: 2)Execute
MainGreen.during:green_ticks = 2 + 1 = 3Result: Minimum green time now satisfied
Cycle 4 (enter pedestrian phase - yellow light):
Check
MainGreen -> PedestrianPhase:request_latched == 1 && green_ticks >= 3satisfiedExecute
MainGreen.exit(none defined)Execute transition effect: -
request_latched = 0(clear the latch) -yellow_ticks = 0(reset yellow timer) -walk_ticks = 0(reset walk timer)Execute
PedestrianPhase.enter(none defined)PedestrianPhase is composite - follow
[*] -> MainYellowExecute
MainYellow.enter(none defined)Execute
MainYellow.during:yellow_ticks = 0 + 1 = 1Result: Yellow light clears vehicle traffic
Cycle 5 (transition to pedestrian walk):
Check
MainYellow -> PedWalk:yellow_ticks >= 1satisfiedExecute
MainYellow.exit(none defined)Execute
PedWalk.enter(none defined)Execute
PedWalk.during:walk_ticks = 0 + 1 = 1Result: Pedestrian crossing signal activates
Cycle 6 (pedestrian crossing continues):
Check
PedWalk -> [*]:walk_ticks >= 2not satisfied (current: 1)Execute
PedWalk.during:walk_ticks = 1 + 1 = 2Result: Pedestrian crossing time satisfied
Cycle 7 (return to main road green):
Check
PedWalk -> [*]:walk_ticks >= 2satisfiedExecute
PedWalk.exit(none defined)Exit to
PedestrianPhaseCheck
PedestrianPhase -> MainGreen: unconditional transitionExecute
PedestrianPhase.exit(none defined)Execute transition effect: -
green_ticks = 0(reset main green timer) -yellow_ticks = 0(reset yellow timer) -walk_ticks = 0(reset walk timer)Execute
MainGreen.enter(none defined)Execute
MainGreen.during:green_ticks = 0 + 1 = 1Result: Main road green restored, system ready for next cycle
Key Points:
request_latchedimplements button request memory (not requiring continuous press)PedestrianPhasecomposite state models real-world sequence: yellow → pedestrian walk → returnMinimum green time (
green_ticks >= 3) prevents excessive main road interruptionPedWalk -> [*]exits to parent, thenPedestrianPhase -> MainGreencompletes the cycleAll timers reset on phase transitions, ensuring clean state for next cycle
Self-transition
MainGreen -> MainGreenallows request latching without changing state
Best Practices
State Machine Design
Keep states focused with clear, single responsibilities
Use hierarchical states to group related states
Minimize aspect actions - use sparingly for cross-cutting concerns
Document abstract actions with comments
Testing and Debugging
Test initialization, all transitions, guards, effects, and termination
Print state and variables after each cycle for debugging
Use abstract handlers to trace execution
Inspect state objects with
runtime.get_current_state_object()
Using Hot Start for Testing
Hot start allows jumping directly to specific states for targeted testing without executing full initialization sequences.
Debugging Specific States:
# Jump directly to error handler to test recovery logic
runtime = SimulationRuntime(
sm,
initial_state="System.ErrorHandler",
initial_vars={"error_code": 42, "retry_count": 3}
)
runtime.cycle()
# Test error recovery without triggering the actual error
State Checkpointing and Recovery:
# Save current state for later restoration
checkpoint = {
'state': runtime.current_state.path,
'vars': runtime.vars.copy()
}
# Later: restore from checkpoint
runtime = SimulationRuntime(
sm,
initial_state=checkpoint['state'],
initial_vars=checkpoint['vars']
)
runtime.cycle() # Continue from saved point
Testing State-Specific Logic:
# Test heating logic at specific temperature
runtime = SimulationRuntime(
sm,
initial_state="WaterHeater.Heating",
initial_vars={"water_temp": 52, "draw_count": 0}
)
# Verify behavior over multiple cycles
for i in range(5):
runtime.cycle()
assert runtime.vars['water_temp'] == 52 + (i+1) * 4
CLI Interactive Testing:
$ pyfcstm simulate -i water_heater.fcstm
# Hot start to specific state
> init WaterHeater.Heating water_temp=52 draw_count=0
> cycle 3
# Quickly test heating behavior
Important: Hot start skips enter actions. Ensure enter actions don’t contain critical initialization logic, or verify behavior manually.
Handler Implementation
Keep handlers simple and focused
Avoid side effects - minimize external state modifications
Use the context API to access runtime state
Add logging for debugging complex interactions
Performance
Limit cycle count to avoid infinite loops
Keep guard expressions simple for faster evaluation
Minimize aspect actions (they execute every cycle)
Use pseudo states to skip aspect actions when not needed
Common Pitfalls
Aspect Action Confusion
Problem: Expecting during before/after (without >>) to execute during the during phase.
Solution: Remember that composite state during before/after only execute during entry/exit transitions ([*] -> Child or Child -> [*]), NOT during the during phase.
Event Scoping Issues
Problem: Events not triggering due to incorrect scoping.
Solution: Understand event scoping - :: creates state-specific events, : creates parent-scoped events, / creates root-scoped events.
Variable Initialization
Problem: Variables not initialized before use.
Solution: Always define variables at the top of the DSL with initial values:
def int counter = 0;
def float temperature = 25.0;
Missing Abstract Handlers
Problem: Abstract actions declared but not implemented, causing runtime errors.
Solution: Implement all abstract handlers before running the simulation and register them with runtime.register_handlers_from_object(handlers).
Summary
The simulation runtime provides a powerful environment for testing and understanding FCSTM state machines:
Core concepts: State types, lifecycle actions, aspect actions, execution semantics
Python usage: Creating runtimes, executing cycles, triggering events, implementing handlers
Execution semantics: Cycle execution, hierarchical execution order
Best practices: Design, testing, debugging, performance optimization
For more information, explore:
FCSTM Visualization Guide - Visualize state machines
PyFCSTM DSL Syntax Tutorial - Advanced DSL features
Template System Tutorial - Code generation from state machines