---
title: 'State Machine Diagrams'
source: 'https://academia.sh/en/courses/modeling-and-representation/state-machine-diagrams'
course: 'Modeling and Representation'
language: en
updated: '2026-08-17T18:08:15+00:00'
license: 'CC BY-SA 4.0'
---

# State Machine Diagrams

Counting the under-approximation and over-approximation of the state, transition, and guard-condition notation separately: a four-state machine rejects two real sequences, and adding two transitions brings the rejected count to zero while raising over-approximation from 2 to 26.

The previous lesson measured a looseness: the activity notation accepted 168 total
orderings at once, and this excess was deliberate. A partial order is written precisely
to be loose; what needed counting was the size of that looseness.

A state machine is not written to be loose. It claims the opposite: it lists the states
an object can pass through and the transitions **permitted** between those states, and
rejects everything else. It is a claim of exactness. This lesson tests that claim from
two directions: does the machine reject a sequence that **genuinely occurs** in the
system, and does it accept a sequence that **never occurs** in the system. The two
errors are distinct things, are counted separately, and closing one grows the other.

## A Two-Way Comparison

A work order in the shop has four states: open, allocated, scheduled, closed. Six
transitions are written. This machine's **language** is every event sequence that
starts at the initial state and ends at the closed state; looking at sequences up to six
events long, this language contains six sequences.

The number of sequences the system actually produces is also six. Because the two
numbers are equal, it might seem the machine is correct. It is not — the sets are not
equal, only their sizes are. There are two things that need measuring:

- **Under-approximation.** A sequence that occurs in the system but that the machine
  rejects. This means the machine holds reality too narrowly; a check written against
  such a machine flags a genuine work order as an error.
- **Over-approximation.** A sequence the machine accepts but that never occurs in the
  system. This means the machine holds reality too broadly; such a machine presents a
  path that will never happen as if it exists, and leads to code being written for that
  path.

```python
"""State machine notation: under-approximation and over-approximation are
counted separately. The STATE_MACHINE, REAL_SEQUENCES, and machine_language
definitions from the shared reference are built exactly as they are there."""
from itertools import product

STATE_MACHINE = {
    "open":      {"allocate": "allocated", "cancel": "closed"},
    "allocated": {"schedule": "scheduled", "cancel": "closed"},
    "scheduled": {"process": "scheduled", "finish": "closed"},
    "closed":    {},
}
START = "open"
EVENTS = ("allocate", "schedule", "process", "finish", "cancel")

REAL_SEQUENCES = [
    ("allocate", "schedule", "process", "finish"),
    ("allocate", "schedule", "process", "process", "finish"),
    ("allocate", "schedule", "process", "process", "process", "finish"),
    ("allocate", "schedule", "process", "schedule", "process", "finish"),   # rescheduled
    ("allocate", "schedule", "cancel"),                                     # canceled before processing
    ("cancel",),
]


def machine_accepts(machine, sequence, start=START):
    state = start
    for event in sequence:
        if event not in machine[state]:
            return False
        state = machine[state][event]
    return True


def machine_language(machine, events, length, start=START):
    accepted = []
    for n in range(1, length + 1):
        for sequence in product(events, repeat=n):
            state = start
            valid = True
            for event in sequence:
                if event not in machine[state]:
                    valid = False
                    break
                state = machine[state][event]
            if valid and not machine[state]:
                accepted.append(sequence)
    return accepted


# ---- repaired machine: two transitions added
REPAIRED = {state: dict(g) for state, g in STATE_MACHINE.items()}
REPAIRED["scheduled"]["schedule"] = "scheduled"
REPAIRED["scheduled"]["cancel"] = "closed"


# ---- DD11: guarded machine. The same two transitions, with a guard condition on top.
def processed_since_last_schedule(history):
    """Has there been at least one 'process' since the state last entered 'scheduled'."""
    position = max((i for i, e in enumerate(history) if e == "schedule"), default=None)
    return position is not None and "process" in history[position + 1:]


GUARDS = {
    ("scheduled", "schedule"): processed_since_last_schedule,
    ("scheduled", "cancel"): lambda history: "process" not in history,
}


def guarded_accepts(sequence, start=START):
    state, history = start, []
    for event in sequence:
        if event not in REPAIRED[state]:
            return False
        guard = GUARDS.get((state, event))
        if guard and not guard(tuple(history)):
            return False
        history.append(event)
        state = REPAIRED[state][event]
    return True


def guarded_language(events, length, start=START):
    accepted = []
    for n in range(1, length + 1):
        for sequence in product(events, repeat=n):
            if not guarded_accepts(sequence, start):
                continue
            state = start
            for event in sequence:
                state = REPAIRED[state][event]
            if not REPAIRED[state]:
                accepted.append(sequence)
    return accepted


# ---- DD12: writing history into the STATE instead of text. "scheduled" splits in two.
SPLIT = {
    "open":       {"allocate": "allocated", "cancel": "closed"},
    "allocated":  {"schedule": "scheduled", "cancel": "closed"},
    "scheduled":  {"process": "processed", "cancel": "closed"},
    "processed":  {"process": "processed", "schedule": "scheduled", "finish": "closed"},
    "closed":     {},
}


def symbol_count(machine, guards=0):
    return len(machine) + sum(len(g) for g in machine.values()) + guards


def measure(name, language, accepts):
    over = [d for d in language if d not in REAL_SEQUENCES]
    under = [d for d in REAL_SEQUENCES if not accepts(d)]
    return {"name": name, "language": len(language), "over": len(over), "under": len(under),
            "under_list": under, "over_list": over}


D1 = machine_language(STATE_MACHINE, EVENTS, 6)
O1 = measure("four states, six transitions", D1, lambda d: machine_accepts(STATE_MACHINE, d))
print("SYSTEM  :", len(REAL_SEQUENCES), "real transition sequences")
print("NOTATION:", symbol_count(STATE_MACHINE), "symbols =", len(STATE_MACHINE), "states +",
      sum(len(g) for g in STATE_MACHINE.values()), "transitions")
print("COST    : under-approximation", O1["under"], "| over-approximation", O1["over"])
print()
for d in O1["under_list"]:
    print("  REJECTED:", " ".join(d))
for d in O1["over_list"]:
    print("  OVER-ACCEPTED:", " ".join(d))
print()
D2 = machine_language(REPAIRED, EVENTS, 6)
O2 = measure("two transitions added", D2, lambda d: machine_accepts(REPAIRED, d))
D3 = guarded_language(EVENTS, 6)
O3 = measure("two transitions + two guard conditions", D3, guarded_accepts)
D4 = machine_language(SPLIT, EVENTS, 6)
O4 = measure("five states, nine transitions", D4, lambda d: machine_accepts(SPLIT, d))
print(f"{'machine':40s} symbols  accepted  under  over")
for o, s in ((O1, symbol_count(STATE_MACHINE)), (O2, symbol_count(REPAIRED)),
             (O3, symbol_count(REPAIRED, 2)), (O4, symbol_count(SPLIT))):
    print(f"{o['name']:40s} {s:5d} {o['language']:6d} {o['under']:6d} {o['over']:6d}")
print()
print("over-approximation depends on the length window")
print("window  six-transition  eight-transition  guarded  split-state")
for u in (4, 5, 6, 7, 8):
    a = len([d for d in machine_language(STATE_MACHINE, EVENTS, u)
             if d not in REAL_SEQUENCES])
    b = len([d for d in machine_language(REPAIRED, EVENTS, u) if d not in REAL_SEQUENCES])
    c = len([d for d in guarded_language(EVENTS, u) if d not in REAL_SEQUENCES])
    e = len([d for d in machine_language(SPLIT, EVENTS, u) if d not in REAL_SEQUENCES])
    print(f"{u:7d} {a:11d} {b:16d} {c:9d} {e:13d}")
print()
print("the guarded machine's over-approximation:", [" ".join(d) for d in O3["over_list"]])
print("the split-state machine's over-approximation:", [" ".join(d) for d in O4["over_list"]])
```

```
SYSTEM  : 6 real transition sequences
NOTATION: 10 symbols = 4 states + 6 transitions
COST    : under-approximation 2 | over-approximation 2

  REJECTED: allocate schedule process schedule process finish
  REJECTED: allocate schedule cancel
  OVER-ACCEPTED: allocate cancel
  OVER-ACCEPTED: allocate schedule finish

machine                                  symbols  accepted  under  over
four states, six transitions                10      6      2      2
two transitions added                       12     32      0     26
two transitions + two guard conditions      14     10      0      4
five states, nine transitions               14      9      0      3

over-approximation depends on the length window
window  six-transition  eight-transition  guarded  split-state
      4           2                5         2             1
      5           2               12         3             2
      6           2               26         4             3
      7           3               58         9             8
      8           4              122        17            16

the guarded machine's over-approximation: ['allocate cancel', 'allocate schedule finish', 'allocate schedule process schedule finish', 'allocate schedule process process schedule finish']
the split-state machine's over-approximation: ['allocate cancel', 'allocate schedule process schedule cancel', 'allocate schedule process process schedule cancel']
```

Three numbers: the system has **6 real sequences**, the notation has **10 symbols**, the
cost is **2 cases of under-approximation and 2 of over-approximation**.

## The Two Rejected Sequences

The two sequences the machine rejects genuinely happen in the shop. The first is
rescheduling: after a work order is processed, a problem turns up, the work is
rescheduled, and processed again. The machine has no transition from the scheduled state
back to the scheduled state, so the second `schedule` event is rejected.

The second is cancellation before processing begins. The machine accepts cancellation
only in the open and allocated states; there is no cancel path from the scheduled state.
Yet a work order that has been scheduled but not yet processed can, in fact, be
canceled.

What the two cases of under-approximation share is that both are **invisible**. The
machine does not say it is wrong; it only does not accept those sequences. Someone
looking at the diagram cannot notice the gap, because what is missing is not drawn in
the diagram. It only comes to light once the list of real sequences is held and tested
against the machine one at a time — which is exactly what this lesson does.

The two over-accepted sequences are drawn the same way but do not occur in reality.
Canceling directly after stock is allocated, and finishing without any processing at all
after scheduling, are paths the machine permits; neither happens in the shop.

## The Cost of Repairing

The way to rescue the two rejected sequences is clear: add two transitions to the
scheduled state. One is `schedule` from scheduled to scheduled, the other is `cancel`
from scheduled to closed. The notation rises from 10 symbols to 12. Under-approximation
drops to **0** — both real sequences are now accepted.

Over-approximation, however, rises from 2 to **26**. Thirteen times. The machine's
language grows from six sequences to 32, and all of the growth comes from sequences with
no counterpart in the system.

The reason is structural. Adding a self-loop `schedule` transition to the scheduled
state does not say "it can be rescheduled after processing"; it says **"it can be
rescheduled at any moment while in the scheduled state, any number of times."** A state
machine keeps no history; the only thing it keeps is the state it is currently in. Once
a transition is added, that transition becomes open after **every** path by which the
state can be reached. It is added for one intended path and opens up for every path
obtained.

The rule that follows is this: **the price of closing under-approximation is
over-approximation.** The two errors sit in opposite directions, and no notation can
bring both to zero at once — if it could, the notation would be the system itself, not a
projection of it. For this reason, a single number is not enough when evaluating a
state machine; two numbers are written side by side.

## A Guard Condition Narrows the Cost

The notation has a tool against this situation: the **guard condition**. A condition is
written on a transition, one that can only be taken once it is satisfied. Let two guard
conditions be added to the two new transitions (**DD11**): rescheduling can only be
taken if at least one processing has happened since the scheduled state was entered;
cancellation can only be taken if no processing has happened at all.

The result is in the table: 14 symbols, under-approximation still **0**,
over-approximation drops from 26 to **4**. Twenty-two wrong sequences were closed off
for the price of two symbols.

Two of the remaining four were already there from the start — canceling directly after
allocating stock, and finishing without any processing after scheduling. So the net
over-approximation brought in by the two transitions and two guard conditions is **2**,
and both of these are the case of rescheduling and then finishing without any
processing. A guard condition does not zero out the cost, it narrows it.

Narrowing has its own cost too. A guard condition is text; it does not sit in the
structure of the state graph but as a sentence written on the transition. The machine's
structure does not test it — what tests it is the code that reads the condition and
implements it. The notation wrote two more symbols but moved testability from the graph
into text, and a constraint written as text can be wrong. Indeed, that is exactly what
happens here: the written condition does not prevent finishing directly after
rescheduling.

## Writing History into the State Instead of Text

What the guard condition is doing is, in fact, **remembering history**: the question
"has there been processing since I entered the scheduled state" asks for information the
state does not carry. There is a second way to carry this information — turn it into a
state.

The scheduled state is split in two (**DD12**): **scheduled** for work that has been
scheduled but not yet processed, **processed** for work that processing has begun on.
Cancellation can only be taken from the scheduled state, finishing only from the
processed state; rescheduling goes from the processed state back to the scheduled
state. The result is five states and nine transitions, that is, **14 symbols** — exactly
the same number as the guarded machine.

The measurement comes out better in two places. Under-approximation is still 0,
over-approximation is **3 instead of 4**. What is more, one of the remaining three was
already there from the start, and the two sequences the guarded machine could not close
— rescheduling and then finishing without any processing at all — are closed
**structurally** here: there is no `finish` transition out of the scheduled state. On
the other hand, the split machine accepts cancellation after rescheduling, and this
sequence does not happen in the shop.

The real difference is not in the count but in where the constraint sits. In the split
machine, the rule is **in the graph itself**: whether a transition exists can be seen
by looking. In the guarded machine, the rule is a sentence, and whether that sentence is
read correctly and implemented correctly cannot be understood by looking at the graph.
The same 14 symbols sit in two different places; one in the notation's testable part,
the other in its untestable part.

## The Number Depends on the Window

The table in the middle carries a warning. A machine's language is, in most cases,
infinite — the `process` loop in the scheduled state produces a sequence at every
length. For this reason "over-approximation 26" is not an absolute number; it is a value
counted within the window of **sequences of at most six events**.

As the window grows, all four machines grow, but at different rates. The base
six-transition machine gives 2, 2, 2, 3, 4 across the window from 4 to 8; the
eight-transition machine gives 5, 12, 26, 58, **122**; the guarded machine gives 2, 3,
4, 9, 17; the split-state machine gives 1, 2, 3, 8, 16. The base machine stays narrow and
so grows slowly; the repaired machine opens new paths at every length and so grows
fast.

What this means for comparison is that two machines' over-approximation can only be
compared **within the same window**, and an over-approximation number given without its
window cannot be read. What the denominator is to a coverage ratio, the window is to
over-approximation.

## Summary

- A state machine can be wrong in two directions at once: **under-approximation** is
  rejecting what occurs in the system, **over-approximation** is accepting what does
  not. The two numbers are written separately; the language and the real sequence count
  being equal does not show the machine is correct.
- The four-state, six-transition machine, with **10 symbols**, **rejects 2** of the
  shop's 6 real sequences (rescheduling, and canceling before processing) and **accepts
  2 sequences** that do not occur in the system.
- Adding two transitions brings under-approximation down to **0** but raises
  over-approximation from 2 to **26**: thirteen times. Because a state machine keeps no
  history, an added transition opens after every path by which the state is reached.
- **The price of closing under-approximation is over-approximation.** No single notation
  can zero out both errors at once; if it could, it would be the system itself, not a
  projection.
- Two guard conditions bring over-approximation from 26 down to **4** with 14 symbols;
  splitting the scheduled state in two brings it down to **3** with the same 14 symbols.
  The difference is not in the count but in whether the constraint sits in the graph or
  in text.
- The over-approximation count depends on the length window: as the window grows from 4
  to 8, the base machine goes from 2 to 4, the repaired machine from 5 to **122**, the
  guarded machine from 2 to 17, the split machine from 1 to 16. An over-approximation
  number given without its window cannot be read.

## Next Step

This topic counted four notations of behavior. The use case notation deliberately
dropped internal steps and failed to distinguish 128 systems; the sequence notation
showed one of 24 traces; the activity notation accepted 168 total orderings at once; the
state machine was wrong in both directions at once. What the four share is that each
tries to show the system's side **in time**.

The next topic does not discuss time at all. It asks what a system **stores**: what
entities exist, what attributes they carry, what relationships are established between
them. The conceptual model states **26 facts** for the repair shop: 5 entities, 12
attributes, 4 relationships, 5 constraints. How these 26 facts behave when brought down
to a schema reveals a different kind of loss than the behavioral notations showed — the
model does not only forget, it also **invents**, and the two are counted separately.
