feat: added documentation
This commit is contained in:
@@ -1,10 +1,210 @@
|
|||||||
# SNanoSM
|
# SNanoSM
|
||||||
|
|
||||||
A pure python, minimalistic, typed library for the implementation of Mealy Finite State Machines.
|
A pure Python, minimalistic, and fully typed library for the implementation of **Mealy Finite State Machines**.
|
||||||
|
|
||||||
Part of the Nobody Industry's MFFP (made from first principles) set of libraries.
|
Part of Nobody Industry's **MFFP (Made From First Principles)** set of libraries.
|
||||||
|
|
||||||
## Features
|
---
|
||||||
|
|
||||||
- Typed
|
## Table of Contents
|
||||||
- Full test coverage
|
1. [Overview & Core Concept](#overview--core-concept)
|
||||||
|
2. [Installation](#installation)
|
||||||
|
3. [Quick Start (Binary Inverter)](#quick-start-binary-inverter)
|
||||||
|
4. [Advanced Usage (Sequence Detector & Fallback Matching)](#advanced-usage-sequence-detector--fallback-matching)
|
||||||
|
5. [API Reference](#api-reference)
|
||||||
|
6. [Testing & Verification](#testing--verification)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview & Core Concept
|
||||||
|
|
||||||
|
A **Mealy State Machine** is a finite-state machine whose output values are determined both by its current state and its current inputs.
|
||||||
|
|
||||||
|
In [mealy.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py), this is modeled by:
|
||||||
|
- **States**: Unique nodes in the machine.
|
||||||
|
- **Transitions**: Directed connections between states triggered by specific inputs.
|
||||||
|
- **Action Functions (Outputs)**: Arbitrary callbacks associated with transitions that receive a mutable user-defined context object.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> By passing a mutable context down to transition actions, you can build rich state-dependent behavior while keeping the machine's state logic simple and decoupled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Since the library uses [pyproject.toml](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/pyproject.toml) with the Hatchling build backend, you can install it locally in editable mode or build it using standard tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install in editable mode
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Or build the package
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start (Binary Inverter)
|
||||||
|
|
||||||
|
Here is a simple example demonstrating how to invert a binary string (`"0"` becomes `"1"`, `"1"` becomes `"0"`) using [inverter.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/examples/inverter.py).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Tuple, TypedDict
|
||||||
|
from snanosm.mealy import Machine
|
||||||
|
|
||||||
|
# Define a context to hold our state machine's output and metadata
|
||||||
|
class Context(TypedDict):
|
||||||
|
result: str
|
||||||
|
n_chars: int
|
||||||
|
|
||||||
|
def add_to_result(context: Context, c: str) -> None:
|
||||||
|
context["result"] += c
|
||||||
|
context["n_chars"] += 1
|
||||||
|
|
||||||
|
def inverter(input_string: str) -> Tuple[str, int]:
|
||||||
|
# Initialize the mutable context
|
||||||
|
context: Context = {
|
||||||
|
"result": "",
|
||||||
|
"n_chars": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the machine with the context
|
||||||
|
m = Machine(context)
|
||||||
|
|
||||||
|
# Add a start state "S"
|
||||||
|
m.add_state("S", is_start_state=True)
|
||||||
|
|
||||||
|
# Define transitions: when in state "S" and input is "0", execute action and stay in "S"
|
||||||
|
m.add_transition("0", "S", "S", lambda ctx: add_to_result(ctx, "1"))
|
||||||
|
m.add_transition("1", "S", "S", lambda ctx: add_to_result(ctx, "0"))
|
||||||
|
|
||||||
|
# Process inputs sequentially
|
||||||
|
for c in input_string:
|
||||||
|
m.process_input(c)
|
||||||
|
|
||||||
|
return context["result"], context["n_chars"]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
res, count = inverter("000011110010")
|
||||||
|
print(f"Result: {res}, Characters processed: {count}")
|
||||||
|
# Output: Result: 111100001101, Characters processed: 12
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Usage (Sequence Detector & Fallback Matching)
|
||||||
|
|
||||||
|
The sequence detector in [detector.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/examples/detector.py) searches for the substring `"AB"` within a stream of characters. It illustrates the use of [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14) to define catch-all transitions when no specific input matches.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import TypedDict
|
||||||
|
from snanosm.mealy import Machine, TransitionInputEnum
|
||||||
|
|
||||||
|
class Context(TypedDict):
|
||||||
|
count: int
|
||||||
|
position: int
|
||||||
|
matches: int
|
||||||
|
|
||||||
|
def dinc(context: Context):
|
||||||
|
context["count"] += 1
|
||||||
|
|
||||||
|
def dset(context: Context):
|
||||||
|
context["position"] = context["count"]
|
||||||
|
context["count"] += 1
|
||||||
|
|
||||||
|
def dprn(context: Context):
|
||||||
|
context["count"] += 1
|
||||||
|
print(f"SUBSTRING FOUND AT POSITION: {context['position']}")
|
||||||
|
context["matches"] += 1
|
||||||
|
|
||||||
|
def detector(input_string: str) -> int:
|
||||||
|
initial_context: Context = {
|
||||||
|
"count": 0,
|
||||||
|
"position": 0,
|
||||||
|
"matches": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
m = Machine(initial_context)
|
||||||
|
m.add_state("Q0", is_start_state=True)
|
||||||
|
m.add_state("Q1")
|
||||||
|
|
||||||
|
# Q0 -> Q1 on 'A', saving the start position
|
||||||
|
m.add_transition("A", "Q0", "Q1", lambda context: dset(context))
|
||||||
|
# Catch-all transition: Q0 -> Q0 for any input other than 'A'
|
||||||
|
m.add_transition(TransitionInputEnum.MATCH_REST, "Q0", "Q0", lambda context: dinc(context))
|
||||||
|
|
||||||
|
# Q1 -> Q0 on 'B', printing match information
|
||||||
|
m.add_transition("B", "Q1", "Q0", lambda context: dprn(context))
|
||||||
|
# Catch-all transition: Q1 -> Q0 for any input other than 'B'
|
||||||
|
m.add_transition(TransitionInputEnum.MATCH_REST, "Q1", "Q0", lambda context: dinc(context))
|
||||||
|
|
||||||
|
for c in input_string:
|
||||||
|
m.process_input(c)
|
||||||
|
|
||||||
|
return initial_context["matches"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Core Abstractions
|
||||||
|
|
||||||
|
| Class/Type | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6) | A typing protocol requiring `__eq__` and `__hash__`. Any hashable, equatable Python object can serve as machine input. |
|
||||||
|
| [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14) | Enum containing special transition inputs (e.g., `MATCH_REST`). |
|
||||||
|
| [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22) | Represents a state node in the state machine. |
|
||||||
|
| [Transition](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L33) | Represents an edge between states triggered by a transition input. |
|
||||||
|
| [Machine](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L53) | The core finite state machine runner. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API Details
|
||||||
|
|
||||||
|
#### [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6)
|
||||||
|
```python
|
||||||
|
class InputProtocol(Protocol):
|
||||||
|
def __eq__(self, __o: Self) -> bool: ...
|
||||||
|
def __hash__(self) -> int: ...
|
||||||
|
```
|
||||||
|
Any custom object used as an input to [Machine.process_input](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L103) must implement this protocol (or be natively hashable and equatable, e.g. strings, integers, frozen dataclasses).
|
||||||
|
|
||||||
|
#### [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14)
|
||||||
|
- `MATCH_REST`: Activates if no matching transition input is found for the current state. Useful for defining default fallback transitions.
|
||||||
|
|
||||||
|
#### [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22)
|
||||||
|
- `get_name() -> str`: Returns the state's name.
|
||||||
|
- `__str__() -> str`: Returns `[State <state_name>]`.
|
||||||
|
|
||||||
|
#### [Transition](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L33)
|
||||||
|
- `execute_output(context)`: Executes the output callback if it was supplied.
|
||||||
|
- `get_destination_name_hash() -> int`: Returns the hash of the destination state name.
|
||||||
|
- `__str__() -> str`: Returns `[Transition (<origin>, <destination>, <input>)]`.
|
||||||
|
|
||||||
|
#### [Machine](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L53)
|
||||||
|
- `__init__(initial_context: object = None)`: Initializes the machine. Sets up the configuration using an optional initial context. If not provided, an empty dict `{}` is instantiated.
|
||||||
|
- `add_state(state_name: str, is_start_state: bool = False, is_end_state: bool = False) -> None`: Registers a new state node in the machine.
|
||||||
|
> [!WARNING]
|
||||||
|
> State names must not start with `#` (reserved for internal configurations). A machine cannot have multiple start states.
|
||||||
|
- `add_transition(transition_input: TransitionInput, origin_name: str, destination_name: str, output_function: Optional[Callable[[Optional[object]], None]]) -> None`: Registers a transition edge between two existing states.
|
||||||
|
- `transition_input`: An input conforming to [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6) or [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14).
|
||||||
|
- `output_function`: A callable accepting context, run upon transitioning.
|
||||||
|
- `process_input(i: Input) -> None`: Processes a single input token. It evaluates transitions registered under the current state.
|
||||||
|
- If a transition matching `i` is registered, it will be executed.
|
||||||
|
- If no matching transition is found but a `MATCH_REST` transition is registered, that fallback is executed.
|
||||||
|
- If no valid transition is found, it raises a `ValueError`.
|
||||||
|
- `reset() -> None`: Resets the state machine's active state back to the start state, and re-assigns the context back to `initial_context`.
|
||||||
|
- `get_current_state() -> Optional[State]`: Returns the current [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22) object, or `None` if the machine has not started or processed any inputs.
|
||||||
|
- `is_in_final_state() -> bool`: Returns `True` if the machine's current state is registered as an end state.
|
||||||
|
- `__str__() -> str`: Returns a structured string layout of the machine structure, lists of states, and transitions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing & Verification
|
||||||
|
|
||||||
|
Unit tests are located in [test_mealy.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/tests/test_mealy.py). To run the test suite, navigate to the project directory and execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src python -m unittest discover -s tests
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user