pyfcstm.simulate.runtime
Simulation runtime for executing hierarchical finite state machines.
This module implements a cycle-based execution engine for state machines parsed from the pyfcstm DSL. The runtime maintains an execution stack of active states, variable mappings, and internal frame modes that control lifecycle progression.
Each cycle advances the state machine until reaching a stable boundary: either a stoppable state (non-pseudo leaf state), termination (empty stack), or validation failure (changes rolled back). The runtime uses speculative validation to ensure transitions can reach valid stable states before executing them.
The execution model uses internal frame modes (active, after_entry, init_wait, post_child_exit) to track execution phases. Transitions are selected from the current stack-top state’s transitions in declaration order. Parent-level transitions are only considered after a child explicitly exits to parent via [*] transitions.
Lifecycle actions execute in a specific order: enter when entering states, during while in leaf states each cycle, and exit when leaving states. For leaf states, the during chain includes ancestor aspect actions (>> during before/after) that execute before and after the leaf’s own during actions. Pseudo states skip ancestor aspect actions entirely.
Composite states have special during before/after actions that execute only at composite boundaries: during before runs when entering from parent ([*] -> Child), and during after runs when exiting to parent (Child -> [*]). These do NOT execute during child-to-child transitions.
Validation works by cloning the execution context and speculatively executing the transition until reaching a stable boundary. If validation succeeds, the real transition executes. If validation fails or exceeds safety limits (1000 steps or 64 stack depth), the transition is rejected and variables roll back.
Abstract actions can be implemented by registering Python handlers that receive read-only execution context. Handlers can be registered individually or organized in classes using the @abstract_handler decorator.
Hot Start Feature:
The runtime supports “hot start” mode, which allows starting execution from an arbitrary state without executing enter actions. This is useful for:
Debugging: Jump directly to a specific state to test behavior
State Recovery: Resume execution from a known state and variable configuration
Testing: Verify state-specific logic without executing full initialization
Hot start is enabled by providing initial_state and initial_vars parameters
to the constructor. The runtime constructs the execution stack directly to the
target state, bypassing all enter actions. For composite states, the runtime
automatically performs initial transitions during the first cycle to find a
stoppable leaf state.
Basic usage:
from pyfcstm.simulate import SimulationRuntime
runtime = SimulationRuntime(state_machine)
runtime.cycle() # Execute first cycle
runtime.cycle(['EventName']) # Execute with events
# Access state and variables
current_state = runtime.current_state
variables = runtime.vars
Hot start usage:
# Start from a specific state with custom variable values
runtime = SimulationRuntime(
state_machine,
initial_state="System.Active",
initial_vars={"counter": 10, "flag": 1}
)
# First cycle starts from Active state without executing enter actions
runtime.cycle()
Abstract handler registration:
def my_handler(ctx):
print(f"State: {ctx.get_full_state_path()}")
runtime.register_abstract_handler('System.Active.Init', my_handler)
# Or use decorator-based registration
from pyfcstm.simulate import abstract_handler
class MyHandlers:
@abstract_handler('System.Active.Init')
def handle_init(self, ctx):
print(f"Counter: {ctx.get_var('counter')}")
runtime.register_handlers_from_object(MyHandlers())
SimulationRuntimeDfsError
- class pyfcstm.simulate.runtime.SimulationRuntimeDfsError[source]
Raised when speculative validation exceeds safety limits without converging.
This exception indicates that the state machine contains an invalid unbounded execution chain that prevents the runtime from reaching a stable stoppable state. The runtime enforces two safety limits during speculative validation:
Step Limit: Maximum 1000 speculative steps per validation attempt
Depth Limit: Maximum 64 structural stack frames
When either limit is exceeded, validation aborts and raises this exception to prevent infinite loops or stack overflow.
Why This Happens:
This error typically occurs when a state machine has:
Composite states with no valid initial transitions
Chains of pseudo states that never reach a stoppable state
Circular dependencies between exit transitions and parent continuations
Guard conditions that create infinite validation loops
Error Prevention:
To avoid this error, ensure your state machine:
Every composite state has at least one unconditional initial transition
Pseudo state chains eventually reach a stoppable leaf state
Exit transitions lead to valid parent continuations
Guard conditions don’t create circular validation dependencies
Example of problematic state machine:
>>> dsl_code = ''' ... state Root { ... state A { ... [*] -> B : if [false]; # Always blocked ... pseudo state B; ... } ... [*] -> A; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() # Raises SimulationRuntimeDfsError
Note
This exception is raised during validation, not during normal execution. If you encounter this error, review your state machine definition for unbounded execution chains or missing stoppable states.
SimulationRuntime
- class pyfcstm.simulate.runtime.SimulationRuntime(state_machine: StateMachine, abstract_error_mode: Literal['raise', 'log'] = 'raise', history_size: int | None = None, initial_state: str | Tuple[str, ...] | State | None = None, initial_vars: Dict[str, int | float] | None = None)[source]
Runtime environment for executing hierarchical finite state machines.
This class provides the stateful execution engine for state machines parsed from the pyfcstm DSL. It maintains an active state stack, variable mappings, and lifecycle flags that control cycle-based execution semantics.
Core Execution Model:
The runtime implements cycle-based execution where each
cycle()call advances the state machine until reaching a stable boundary. Cycles can end in three ways:Stoppable State: Reached a leaf state (non-pseudo) where execution can stabilize
Termination: The state machine has ended (empty stack)
Validation Failure: Cannot reach a stoppable state, changes rolled back
Transition Selection:
Transitions are always selected from the current stack-top state’s
transitions_fromlist in declaration order. This is critical for understanding cross-level transition behavior:A leaf state can only fire its own transitions
Parent-level transitions are only considered after the child exits to parent
This explains why some transitions require explicit exit transitions first
For example, if state
System1.Awants to trigger a parent-level transitionSystem1 -> System2, it must first exit to parent viaA -> [*], which putsSystem1inpost_child_exitmode where parent-level transitions can be considered.Validation and Rollback:
When a stoppable state has an enabled transition, the runtime performs speculative validation to ensure the transition can eventually reach another stoppable state or terminate. This prevents cycles from getting stuck in non-stoppable configurations.
If validation fails or a cycle cannot reach a stoppable state, all variable changes are rolled back and the runtime remains at the previous stable boundary. For the initial cycle, rollback pins the runtime at the root boundary in
init_waitmode.DFS Safety Limits:
To prevent infinite loops in pathological state machines, validation enforces two safety limits:
Maximum 1000 speculative steps per validation attempt
Maximum 64 structural stack depth
Repeated execution states are pruned automatically
When these limits are exceeded,
SimulationRuntimeDfsErroris raised.- Parameters:
state_machine (StateMachine) – The state machine model to simulate.
- Variables:
state_machine (StateMachine) – The state machine being simulated.
stack (List[_Frame]) – Active frames ordered from root to current execution point.
vars (Dict[str, Union[int, float]]) – Mutable variable values visible to guards, effects, and actions.
_initialized (bool) – Whether the runtime has performed root entry.
_ended (bool) – Whether execution has terminated (empty stack).
Example:
>>> 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 { ... state Idle { ... during { counter = counter + 1; } ... } ... state Active { ... during { counter = counter + 10; } ... } ... [*] -> Idle; ... Idle -> Active :: Start; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.current_state.path ('System', 'Idle') >>> runtime.vars['counter'] 1 >>> runtime.cycle(['System.Idle.Start']) >>> runtime.current_state.path ('System', 'Active') >>> runtime.vars['counter'] 11
- __init__(state_machine: StateMachine, abstract_error_mode: Literal['raise', 'log'] = 'raise', history_size: int | None = None, initial_state: str | Tuple[str, ...] | State | None = None, initial_vars: Dict[str, int | float] | None = None)[source]
Initialize the simulation runtime with a state machine model.
This constructor prepares the runtime for execution by initializing variable storage from the state machine’s variable definitions and setting up the initial execution stack with the root state. Variables are initialized in declaration order, allowing later initializers to reference earlier variables.
The runtime stack is initialized with the root state in
init_waitmode, allowingcurrent_stateto be accessed immediately. Full initialization (entering the root state and executing lifecycle actions) is deferred until the firstcycle()call.Hot Start Mode:
If
initial_stateis provided, the runtime performs a “hot start” by directly constructing the execution stack to the specified state without executing enter actions. This simulates having already entered and stabilized at that state. Theinitial_varsparameter allows overriding variable values for the hot start.- Parameters:
state_machine (StateMachine) – The state machine model to simulate.
abstract_error_mode (Literal['raise', 'log']) – Error handling mode for abstract handler exceptions.
'raise'(default) stops execution and raises the exception.'log'logs the exception and continues execution.history_size (Optional[int]) – Maximum number of history entries to keep.
None(default) means unlimited history.initial_state (Optional[Union[str, Tuple[str, ...], State]]) – Optional initial state for hot start. If provided, the runtime will start from this state without executing enter actions. Supports string path (
"System.Active"), tuple path (('System', 'Active')), or State object. Defaults toNone(start from root state).initial_vars (Optional[Dict[str, Union[int, float]]]) – Optional variable overrides for hot start. Only variables defined in the state machine can be overridden. Defaults to
None.
- Returns:
None.- Return type:
None
Example - Default initialization:
>>> 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 x = 10; ... def int y = x + 5; # Can reference earlier variables ... state Root { ... state A; ... [*] -> A; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.vars {'x': 10, 'y': 15} >>> runtime.is_ended False >>> runtime.current_state.name 'Root' >>> runtime.current_state.path ('Root',)
Example - Hot start from specific state:
>>> dsl_code = ''' ... def int counter = 0; ... state System { ... state Idle { ... during { counter = counter + 1; } ... } ... state Active { ... during { counter = counter + 10; } ... } ... [*] -> Idle; ... Idle -> Active :: Start; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime( ... sm, ... initial_state="System.Active", ... initial_vars={"counter": 10} ... ) >>> runtime.current_state.path ('System', 'Active') >>> runtime.vars['counter'] 10 >>> runtime.cycle() # First cycle starts from Active state >>> runtime.vars['counter'] 20
Note
Variable initialization and stack setup happen during construction, but state entry actions are deferred until the first
cycle()call. This allows inspection of initial variable values and the root state before execution begins.Note
Hot start mode constructs the stack without executing enter actions, simulating having already entered the target state. For composite states, the runtime will automatically attempt initial transitions to find a stoppable leaf state during the first cycle.
- property abstract_handler_errors: List[Tuple[str, Exception]]
Get list of abstract handler errors (only in ‘log’ mode).
When
abstract_error_modeis'log', exceptions from abstract handlers are logged and collected here instead of stopping execution.- Returns:
List of (action_path, exception) tuples
- Return type:
List[Tuple[str, Exception]]
Example:
>>> runtime = SimulationRuntime(sm, abstract_error_mode='log') >>> runtime.cycle() >>> for action_path, exception in runtime.abstract_handler_errors: ... print(f"Error in {action_path}: {exception}")
- property brief_stack: List[Tuple[Tuple[str, ...], str]]
Return a compact representation of the active execution stack.
Each tuple contains the state’s full path and the frame’s internal mode. This representation is useful for debugging, testing, and understanding the runtime’s current execution phase without inspecting internal frame objects directly.
Frame Modes:
active: Normal execution stateafter_entry: Just entered a leaf state, during already executedinit_wait: Composite state waiting for initial transitionpost_child_exit: Parent state after child exited via[*]
- Returns:
List of
(state_path, mode)tuples from root to current state.- Return type:
List[Tuple[Tuple[str, …], str]]
Example:
>>> 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 = ''' ... state System { ... state SubSystem { ... state Active; ... [*] -> Active; ... } ... [*] -> SubSystem; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.brief_stack [(('System',), 'active'), (('System', 'SubSystem'), 'active'), (('System', 'SubSystem', 'Active'), 'active')]
Note
This property is primarily useful for testing and debugging. It provides insight into the runtime’s internal execution state without exposing the internal
_Frameobjects.
- clear_all_abstract_handlers() int[source]
Clear all registered abstract handlers.
- Returns:
Total number of handlers removed
- Return type:
int
Example:
>>> count = runtime.clear_all_abstract_handlers() >>> print(f"Cleared {count} handlers")
- property current_state: State
Get the current active state at the top of the execution stack.
This property returns the state currently being executed by the runtime. For leaf states, this is the state where during actions execute. For composite states in
init_waitmode, this is the parent waiting for an initial transition.- Returns:
The current active state.
- Return type:
- Raises:
IndexError – If the runtime has ended and the stack is empty.
Example:
>>> 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 = ''' ... state System { ... state Idle; ... state Active; ... [*] -> Idle; ... Idle -> Active :: Start; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.current_state.name 'System' >>> runtime.current_state.path ('System',) >>> runtime.cycle() >>> runtime.current_state.name 'Idle' >>> runtime.cycle(['System.Idle.Start']) >>> runtime.current_state.name 'Active'
Note
Before the first
cycle()call, this returns the root state. After the runtime has ended, accessing this property will raise an IndexError.
- cycle(events: List[str | Event] | None = None)[source]
Execute a full runtime cycle until reaching a stable boundary.
This method advances the state machine through transitions and lifecycle actions until one of three conditions is met:
Stoppable State: Reached a leaf state (non-pseudo) where execution can stabilize
Termination: The state machine has ended (empty stack)
Validation Failure: Cannot reach a stoppable state, changes rolled back
Cycle Execution Flow:
The cycle operates in two phases:
Speculative Execution: Clone the current context and simulate the cycle to validate it can reach a stable boundary
Commit or Rollback: If validation succeeds, commit the changes; otherwise, rollback to the previous stable state
Event Handling:
Events can be provided as either event objects or dot-separated path strings. Multiple events can be active simultaneously, allowing complex transition chains to execute in a single cycle.
Flexible Event Path Formats:
The runtime supports multiple event path formats for maximum convenience:
Full paths:
"Root.System.Active.Start"- Complete path from rootRelative paths:
"Start"- Resolved from current stateParent-relative:
".error"or"..system.error"- Navigate up hierarchyAbsolute:
"/global.shutdown"- Resolved from root state
The runtime intelligently determines which resolution method to use based on the path syntax and current runtime state. This allows you to use the most convenient format for your use case without worrying about the details.
Validation and Safety:
When a stoppable state has an enabled transition, the runtime validates that taking the transition can eventually reach another stoppable state or terminate. This prevents cycles from getting stuck in non-stoppable configurations.
Validation enforces safety limits: - Maximum 1000 speculative steps per validation - Maximum 64 structural stack depth - Repeated execution states are pruned automatically
Rollback Behavior:
If a cycle cannot reach a stoppable state, all variable changes are rolled back: - For normal cycles: Restore previous stack and variables - For initial cycle: Pin runtime at root boundary in
init_waitmode - All side effects from failed validation are discarded- Parameters:
events (List[Union[str, Event]], optional) – Events available for the current cycle. Can be event objects or dot-separated path strings.
- Returns:
None.- Return type:
None
Example - Basic cycle execution:
>>> 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 { ... state Idle { ... during { counter = counter + 1; } ... } ... state Active { ... during { counter = counter + 10; } ... } ... [*] -> Idle; ... Idle -> Active :: Start; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() # Initialize and reach Idle >>> runtime.current_state.path ('System', 'Idle') >>> runtime.vars['counter'] 1 >>> runtime.cycle(['System.Idle.Start']) # Transition to Active >>> runtime.current_state.path ('System', 'Active')
Example - Flexible event path formats:
>>> dsl_code = ''' ... state Root { ... event global_stop; ... state System { ... event system_error; ... state Idle { ... event start; ... } ... state Active { ... event pause; ... } ... [*] -> Idle; ... Idle -> Active :: start; ... Active -> Idle :: pause; ... } ... [*] -> System; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.current_state.path ('Root', 'System', 'Idle') >>> # Use relative path from current state - most convenient! >>> runtime.cycle(['start']) >>> runtime.current_state.path ('Root', 'System', 'Active') >>> # Use parent-relative path to access parent's event >>> runtime.cycle(['.system_error']) # Access System.system_error >>> # Use absolute path to access root event >>> runtime.cycle(['/global_stop']) # Access Root.global_stop >>> # Full path still works for backward compatibility >>> runtime.cycle(['Root.System.Active.pause']) >>> runtime.current_state.path ('Root', 'System', 'Idle')
Example - Exit transitions and parent continuation:
>>> dsl_code = ''' ... def int x = 0; ... state System1 { ... state A { ... during { x = x + 1; } ... } ... [*] -> A; ... A -> [*] :: Exit; ... } ... state System2 { ... state B { ... during { x = x + 10; } ... } ... [*] -> B; ... } ... [*] -> System1; ... System1 -> System2 :: Switch; ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.current_state.path ('System1', 'A') >>> # Provide both Exit and Switch events >>> runtime.cycle(['System1.A.Exit', 'Switch']) >>> runtime.current_state.path ('System2', 'B')
Example - Validation preventing invalid transitions:
>>> dsl_code = ''' ... state Root { ... state A; ... state B { ... [*] -> C : if [false]; # Blocked initial transition ... state C; ... } ... [*] -> A; ... A -> B :: Go; # Rejected by validation ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.cycle(['Root.A.Go']) # Rejected - cannot reach stoppable >>> runtime.current_state.path ('Root', 'A') # Remains in A due to validation failure
Example - Multiple cycles with state changes:
>>> dsl_code = ''' ... def int counter = 0; ... state Root { ... state A { ... during { counter = counter + 1; } ... } ... [*] -> A; ... A -> A : if [counter >= 3]; # Self-transition after 3 cycles ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.cycle() >>> runtime.vars['counter'] 1 >>> runtime.cycle() >>> runtime.vars['counter'] 2 >>> runtime.cycle() >>> runtime.vars['counter'] 3 >>> runtime.cycle() # Self-transition fires >>> runtime.vars['counter'] 4 # Exit + re-enter + during
Note
Once the runtime has ended (
is_endedisTrue), subsequentcycle()calls are no-ops. Create a newSimulationRuntimeinstance to restart execution.Warning
If validation exceeds safety limits (1000 steps or 64 stack depth),
SimulationRuntimeDfsErroris raised. This indicates an invalid state machine with unbounded execution chains.
- property error_info: Tuple[str, Exception] | None
Get error information if runtime is in error state.
- Returns:
Tuple of (action_path, exception) if in error state,
Noneotherwise.- Return type:
Optional[Tuple[str, Exception]]
Example:
>>> if runtime.is_error_state: ... action_path, exception = runtime.error_info ... print(f"Error in {action_path}: {exception}")
- get_abstract_handlers(action_path: str) List[Callable[[ReadOnlyExecutionContext], None]][source]
Get all registered handlers for an abstract action.
- Parameters:
action_path (str) – Full path to the abstract action
- Returns:
List of registered handlers (may be empty)
- Return type:
List[Callable[[ReadOnlyExecutionContext], None]]
Example:
>>> handlers = runtime.get_abstract_handlers("System.Active.InitHardware") >>> print(f"Found {len(handlers)} handlers")
- has_abstract_handlers(action_path: str) bool[source]
Check if any handlers are registered for an abstract action.
- Parameters:
action_path (str) – Full path to the abstract action
- Returns:
Trueif at least one handler is registered- Return type:
bool
Example:
>>> if runtime.has_abstract_handlers("System.Active.InitHardware"): ... print("Handlers registered")
- property is_ended: bool
Indicate whether the runtime has finished execution.
Once
True, the state machine has terminated and the execution stack is empty. Subsequentcycle()calls become no-ops. To restart execution, create a newSimulationRuntimeinstance.Termination Conditions:
The runtime ends when: - The root state exits to
[*]- An exit transition from a root-level state completes - The execution stack becomes empty for any reason- Returns:
Trueif execution has ended,Falseotherwise.- Return type:
bool
Example:
>>> 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 { ... state Active { ... during { counter = counter + 1; } ... } ... [*] -> Active; ... Active -> [*] : if [counter >= 3]; ... } ... ''' >>> ast = parse_with_grammar_entry(dsl_code, 'state_machine_dsl') >>> sm = parse_dsl_node_to_state_machine(ast) >>> runtime = SimulationRuntime(sm) >>> runtime.is_ended False >>> runtime.cycle() >>> runtime.is_ended False >>> runtime.cycle() >>> runtime.is_ended False >>> runtime.cycle() >>> runtime.is_ended False >>> runtime.cycle() # counter >= 3, exit transition fires >>> runtime.is_ended True >>> runtime.cycle() # No-op, runtime already ended >>> runtime.is_ended True
Note
Once the runtime has ended, the only way to restart execution is to create a new
SimulationRuntimeinstance with the same or a different state machine model.
- property is_error_state: bool
Indicate whether the runtime is in an error state.
When
abstract_error_modeis'raise'and an abstract handler raises an exception, the runtime enters an error state. In this state, the runtime preserves the error information and prevents further execution.- Returns:
Trueif runtime is in error state,Falseotherwise.- Return type:
bool
Example:
>>> runtime = SimulationRuntime(sm, abstract_error_mode='raise') >>> runtime.is_error_state False >>> # After handler raises exception >>> runtime.is_error_state True
- register_abstract_handler(action_path: str, handler: Callable[[ReadOnlyExecutionContext], None]) None[source]
Register a Python function to handle an abstract action.
Multiple handlers can be registered for the same abstract action. They will be executed in registration order.
- Parameters:
action_path (str) – Full path to the abstract action (e.g., “System.Active.InitHardware”)
handler (Callable[[ReadOnlyExecutionContext], None]) – Python function that receives read-only context
- Raises:
ValueError – If action_path is empty or invalid
Example:
>>> def my_init(ctx: ReadOnlyExecutionContext): ... print(f"Initializing in state {ctx.get_state_name()}") ... print(f"Counter value: {ctx.get_var('counter')}") >>> >>> runtime.register_abstract_handler("System.Active.InitHardware", my_init) >>> >>> # Register another handler for the same action >>> def my_init2(ctx: ReadOnlyExecutionContext): ... print(f"Second handler for {ctx.action_name}") >>> >>> runtime.register_abstract_handler("System.Active.InitHardware", my_init2)
- register_handlers_from_object(obj: object) int[source]
Register all decorated methods from an object as abstract handlers.
This method scans the object for methods decorated with
abstract_handler()and automatically registers them with their specified action paths.This provides a convenient way to organize multiple related handlers in a single class, maintaining state and shared logic between handlers.
- Parameters:
obj (object) – Object instance containing decorated handler methods
- Returns:
Number of handlers registered
- Return type:
int
Example:
>>> from pyfcstm.simulate import abstract_handler >>> class MyHandlers: ... def __init__(self): ... self.init_count = 0 ... self.monitor_count = 0 ... ... @abstract_handler('System.Active.Init') ... def handle_init(self, ctx: ReadOnlyExecutionContext): ... self.init_count += 1 ... print(f"Init called {self.init_count} times") ... ... @abstract_handler('System.Active.Monitor') ... def handle_monitor(self, ctx: ReadOnlyExecutionContext): ... self.monitor_count += 1 ... counter = ctx.get_var('counter') ... print(f"Monitor: counter={counter}, called {self.monitor_count} times") ... ... def helper_method(self): ... # Not decorated, won't be registered ... pass >>> >>> handlers = MyHandlers() >>> count = runtime.register_handlers_from_object(handlers) >>> print(f"Registered {count} handlers") Registered 2 handlers
Note
Only methods decorated with
abstract_handler()will be registered. Other methods are ignored.
- unregister_abstract_handler(action_path: str, handler: Callable[[ReadOnlyExecutionContext], None] | None = None) int[source]
Unregister abstract handler(s) for an action.
If handler is
None, removes all handlers for the action. If handler is provided, removes only that specific handler.- Parameters:
action_path (str) – Full path to the abstract action
handler (Optional[Callable[[ReadOnlyExecutionContext], None]]) – Specific handler to remove, or
Noneto remove all
- Returns:
Number of handlers removed
- Return type:
int
Example:
>>> # Remove all handlers for an action >>> count = runtime.unregister_abstract_handler("System.Active.InitHardware") >>> print(f"Removed {count} handlers") >>> >>> # Remove specific handler >>> runtime.unregister_abstract_handler("System.Active.InitHardware", my_init)