---
title: 'Modeling and Representation'
source: 'https://academia.sh/en/courses/modeling-and-representation'
language: en
updated: '2026-08-17T04:39:37+00:00'
license: 'CC BY-SA 4.0'
lessons: 10
outcomes:
    - 'Counts how many distinct systems a representation collapses onto the same symbol'
    - 'Distinguishes the executions a behavioral representation covers from those it does not'
    - 'Counts separately the constraint dropped and the decision invented on the way from a conceptual model to a physical schema'
---

# Modeling and Representation

Counting how much of a system a structural, behavioral or data representation carries, what it cannot tell apart, and what a change of level drops or invents.

## Structural Diagrams

The information elements a notation family carries, the ambiguity the relationship type and multiplicity symbols take on in class notation, and the partitions the component and deployment notations distinguish.

### Notation Family and Loss

Source: https://academia.sh/en/courses/modeling-and-representation/notation-family-and-loss

A representation is a projection of a system that discards information: a structural representation that writes 20 of the 31 facts of a five-association maintenance workshop collapses 2048 distinct systems onto a single diagram, and the 11 discarded fields cannot be recovered from the representation.

The Operating System Concepts course counted a machine's abstractions across fifteen
lessons: the cost each abstraction imposed was measured, and no unfavorable result
was hidden. What those measurements had in common was this — every one of them was
the output of an executable text, and the run itself carried its correctness.
Describing a system to someone else is not that. Describing means showing the system
with **less information**; if the representation were the whole system, it would be
a copy, not a description.

Showing with less information has a cost, and this course counts it. The maintenance
workshop model has five associations and thirty-one facts; a structural
representation that writes twenty of these becomes **indistinguishable from 2047
other systems** that produce the same drawing. The diagram is not wrong; it is
incomplete, and the incompleteness does not show on the diagram itself.

#### The System Is Data; the Representation Is Its Projection

A single fictional domain is used throughout the course: a maintenance workshop. A
work order is opened, split into line items, the line items require parts, a
technician is assigned, the technician uses a machine, and the work order produces a
document. This domain itself is not a diagram, it is **data**: associations, flows,
prerequisites, states, and entities are each a Python structure. The representation
is a projection of that data, and every projection discards information.

The projection measured in this lesson is the narrowest one: a structural
representation that writes only an association's source, target, direction, and
multiplicity.

- **SD1** — The system consists of five associations, and each association carries
  seven fields. Four show up in the representation (source, target, direction,
  **multiplicity**), three do not (lifecycle, ordered, unique).
- **SD2** — On the side whose multiplicity is one, ordering and uniqueness are
  undefined. An N:1 association has one hidden field; 1:N and N:M associations have
  three; eleven in total across five associations.
- **SD3** — Every hidden field takes a binary value: lifecycle is independent or
  dependent, ordered and unique are true or false.
- **SD4** — The count is an **exhaustive count**. Every combination of hidden fields
  is generated, and the ones whose representation stays unchanged are counted; there
  is no estimate, sample, or randomness.

```python
"""M01/K06 shared definition (excerpt): the system IS DATA, the representation is a
projection of that data. The fictional domain is a maintenance workshop. Every
association carries fields that SHOW UP and DO NOT SHOW UP in the representation."""
from itertools import product

ASSOCIATIONS = [
    {"name": "a1", "source": "WorkOrder", "target": "Technician", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a2", "source": "WorkOrder", "target": "LineItem", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": False},
    {"name": "a3", "source": "LineItem",  "target": "Part", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a4", "source": "Technician",   "target": "Machine", "direction": "two-way", "multiplicity": "N:M",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a5", "source": "WorkOrder", "target": "Document", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": True},
]


def hidden_fields(association):
    """On the side whose multiplicity is 1, ordering and uniqueness do not exist."""
    fields = ["lifecycle"]
    if association["multiplicity"] in ("1:N", "N:M"):
        fields += ["ordered", "unique"]
    return fields


def structural_view(associations, detail=0):
    """detail 0: source, target, direction, multiplicity. 1: + lifecycle. 2: + ordered and unique."""
    symbols = []
    for assoc in associations:
        s = {"source": assoc["source"], "target": assoc["target"], "direction": assoc["direction"], "multiplicity": assoc["multiplicity"]}
        if detail >= 1:
            s["lifecycle"] = assoc["lifecycle"]
        if detail >= 2:
            for field in hidden_fields(assoc):
                s[field] = assoc[field]
        symbols.append(s)
    return symbols


def symbol_count(view):
    return sum(len(s) for s in view)


def same_view_count(associations, detail=0):
    """How many distinct systems produce the same representation. The count is exhaustive."""
    options = {"lifecycle": ("independent", "dependent"), "ordered": (False, True),
               "unique": (False, True)}
    goal_view = structural_view(associations, detail)
    spaces = []
    for assoc in associations:
        fields = hidden_fields(assoc)
        spaces.append([dict(zip(fields, d)) for d in product(*[options[f] for f in fields])])
    count = 0
    for choice in product(*spaces):
        candidate = [dict(assoc, **c) for assoc, c in zip(associations, choice)]
        if structural_view(candidate, detail) == goal_view:
            count += 1
    return count


V = structural_view(ASSOCIATIONS, 0)
hidden = sum(len(hidden_fields(a)) for a in ASSOCIATIONS)
print("system :", "associations", len(ASSOCIATIONS), "| visible fields", symbol_count(V),
      "| hidden fields", hidden, "| total facts", symbol_count(V) + hidden)
print("view   :", symbol_count(V), "symbols")
print("cost   :", same_view_count(ASSOCIATIONS, 0), "systems collapse onto the same view")
print()
print("lines written:")
for s in V:
    print("  ", s["source"], s["direction"], s["target"], s["multiplicity"])
print()
print("a2 (WorkOrder -> LineItem), every option for the fields left unwritten:")
for lifecycle in ("independent", "dependent"):
    for ordered in (False, True):
        for unique in (False, True):
            candidate = [dict(a) for a in ASSOCIATIONS]
            candidate[1].update(lifecycle=lifecycle, ordered=ordered, unique=unique)
            print(f"   lifecycle={lifecycle:12s} ordered={str(ordered):5s} unique={str(unique):5s}"
                  f"  same view: {structural_view(candidate, 0) == V}")
```

```
system : associations 5 | visible fields 20 | hidden fields 11 | total facts 31
view   : 20 symbols
cost   : 2048 systems collapse onto the same view

lines written:
   WorkOrder one-way Technician N:1
   WorkOrder one-way LineItem 1:N
   LineItem one-way Part N:1
   Technician two-way Machine N:M
   WorkOrder one-way Document 1:N

a2 (WorkOrder -> LineItem), every option for the fields left unwritten:
   lifecycle=independent  ordered=False unique=False  same view: True
   lifecycle=independent  ordered=False unique=True   same view: True
   lifecycle=independent  ordered=True  unique=False  same view: True
   lifecycle=independent  ordered=True  unique=True   same view: True
   lifecycle=dependent    ordered=False unique=False  same view: True
   lifecycle=dependent    ordered=False unique=True   same view: True
   lifecycle=dependent    ordered=True  unique=False  same view: True
   lifecycle=dependent    ordered=True  unique=True   same view: True
```

Three numbers sit side by side here, and they will sit side by side in every lesson
from here on. **System:** five associations, thirty-one facts. **Representation:**
twenty symbols. **Cost:** 2048 systems collapsing onto the same representation —
that is, the diagram that gets written is the shared picture of 2048 genuinely
different systems.

#### An Unwritten Field Cannot Be Recovered

The output's final block shows the cost on a single association. The `a2`
association carries a work order's line items and sits in the representation as a
single line: one-way from work order to line item, multiplicity 1:N. This line has
three things it **does not say**. Do the line items live and die with the work
order, or do they persist after the work order closes. Do the line items have an
order, or are they a set. Can the same line item appear twice.

Three binary questions make eight options, and all eight produce **exactly the
same** representation. Someone looking at the diagram cannot answer these three
questions; the only place they can ask is the system itself. Taken together, the
eleven hidden fields across the five associations give $2^{11} = 2048$.

The real point is not the size of the number but its direction. The projection is
one-way: there is a single path from the system to the representation, and there are
2048 paths back from the representation to a system. Whoever reads the diagram has
to pick one of these 2048 systems, and **does not notice which one they picked**,
because the diagram does not say that a choice was made. This is the most expensive
part of the loss: the loss itself is invisible on the representation.

#### The Notation Family: Structure, Behavior, Data

The notation family used in these lessons is defined in a specification, and its
name is **UML (Unified Modeling Language)**. The specification is the definition of
a **notation**, not a tool: it fixes which element carries which meaning. The
subject of this course is not the specification itself, but **how much information
the projections it permits discard**.

This course groups the family into three; the specification's own split is twofold —
**structure** and **behavior**. The third group, the data representations, is not part of
UML but comes from the entity–relationship tradition and is a separate notation family.
All three are separate projections of the same system. **Structural representations**
carry the system's static relationships: which type relates to what, how many there
are, in which runtime unit. **Behavioral representations** carry the system's operation
over time: messages, sequences, states, transitions. **Data representations** carry the stored
facts: entities, attributes, relationships, constraints.

The table below places the loss of the three groups side by side. The structure row
was measured in this lesson; the other two rows are among the shared definition's
verified readings and will be measured in detail in their respective topics.

| Family | Facts in the system | What the representation carries | What cannot be recovered |
|---|---|---|---|
| Structure | 5 associations, 31 fields | 20 symbols | 11 fields; 2048 systems collapse onto one diagram |
| Behavior | 24 traces | one sequence diagram: 1 trace | 23 traces; coverage 0.0417 |
| Data | 26 facts (5 entities, 12 attributes, 4 relationships, 5 constraints) | 3 constraints carried into the relational structure | 2 constraints are dropped, 114 decisions are not written in the model |

None of the three rows covers the others, and combining all three still does not
bring the system back. The family is not made of parts of a whole; it is **three
separate losses of the same whole**.

The Architectural Decisions and Documentation course in the Software Architecture
curriculum measured these types along a different axis: it mapped eighteen questions
onto four types, and counted the information element the wrong type leaves out and
the maintenance cost of each type. That course's question was **which type to
choose**. This course's question is different, and it starts after the type has
been chosen: how many systems the chosen type fails to distinguish from one
another. The question-to-type mapping is not rebuilt here.

#### The Representation Grows Linearly, the Ambiguity Grows Exponentially

Per the shared definition's resolution rule, every lesson tests its count with **a
second configuration**. The test here is this: when a sixth association is added to
the system, do the representation and the ambiguity grow at the same rate.

- **SD5** — The added association adds four symbols to the representation; this is
  independent of its multiplicity.
- **SD6** — The number of hidden fields for the added association depends on its
  multiplicity: one if N:1, three if 1:N or N:M.

```python
"""Second configuration: adding one association, the representation and the ambiguity
do not grow at the same rate. The hidden field rule is from the shared definition:
lifecycle, ordered and unique if multiplicity is 1:N or N:M, otherwise only lifecycle.
Because the visible fields are fixed, the count of systems collapsing onto the same
view is the exhaustive count of the hidden field options."""
from itertools import product


def hidden_fields(multiplicity):
    return ["lifecycle", "ordered", "unique"] if multiplicity in ("1:N", "N:M") else ["lifecycle"]


def exhaustive_count(multiplicities):
    spaces = [list(product(*[(False, True)] * len(hidden_fields(m)))) for m in multiplicities]
    return sum(1 for _ in product(*spaces))


BASE = ["N:1", "1:N", "N:1", "N:M", "1:N"]
CONFIGURATIONS = [("five associations (base)", BASE),
                   ("+ Technician-Document   N:1", BASE + ["N:1"]),
                   ("+ Machine-Part          1:N", BASE + ["1:N"]),
                   ("+ LineItem-Machine      N:M", BASE + ["N:M"])]

print("configuration                assoc  symbols  hidden fields  same view")
for label, mults in CONFIGURATIONS:
    print(f"{label:28s}{len(mults):5d}{4 * len(mults):9d}"
          f"{sum(len(hidden_fields(m)) for m in mults):15d}{exhaustive_count(mults):11d}")
print()
base = exhaustive_count(BASE)
for label, mults in CONFIGURATIONS[1:]:
    print(f"{label:28s} symbols x{4 * len(mults) / 20:.2f}   ambiguity x{exhaustive_count(mults) // base}")
```

```
configuration                assoc  symbols  hidden fields  same view
five associations (base)        5       20             11       2048
+ Technician-Document   N:1     6       24             12       4096
+ Machine-Part          1:N     6       24             14      16384
+ LineItem-Machine      N:M     6       24             14      16384

+ Technician-Document   N:1  symbols x1.20   ambiguity x2
+ Machine-Part          1:N  symbols x1.20   ambiguity x8
+ LineItem-Machine      N:M  symbols x1.20   ambiguity x8
```

All three additions put **the same number** of symbols into the representation:
four. The diagram grows by twenty percent in every case. The ambiguity, however,
multiplies by two, eight, and eight. The same drawing effort turns 2048 into either
4096 or 16384, depending on which association was added.

The source of the difference is multiplicity. On the opposite side of an N:1
association, a single object sits; ordering and uniqueness cannot be asked there, so
the only thing stored is the lifecycle field. On the opposite side of a 1:N or N:M
association, a collection sits, and whether the collection has an order and whether
it accepts repeats are two separate facts. The multiplicity symbol is therefore the
most loaded symbol on the diagram: while it states one fact, it also announces that
**two more facts exist**, and it does not write those two facts.

The representation grows **linearly**, with a fixed number of symbols per
association; the ambiguity grows as the **exponent** of the hidden field count. That
the share of the story the diagram tells shrinks as the system grows is not
carelessness — it is the consequence of the gap between two growth rates.

#### The Cost of Removing a Symbol

The same measure works in reverse too. A twenty-symbol representation can be reduced
to a fifteen-symbol one that does not write multiplicity; which associations connect
to whom stays, how many of each drops.

- **SD7** — When multiplicity is not written, an association's multiplicity could be
  any of four options: 1:1, N:1, 1:N, or N:M. Because multiplicity is unknown, **how
  many** hidden fields depend on it is also unknown.

```python
"""A narrower representation: what happens if the multiplicity symbol is never written.
Once multiplicity is unwritten, both the multiplicity itself and the hidden fields that
depend on it become unknown; the number of valid systems for one association is the sum,
over the four multiplicities, of their hidden field options."""
from itertools import product

MULTIPLICITIES = ("1:1", "N:1", "1:N", "N:M")


def hidden_fields(multiplicity):
    return ["lifecycle", "ordered", "unique"] if multiplicity in ("1:N", "N:M") else ["lifecycle"]


def association_options(multiplicity_written, multiplicity):
    """The number of systems that leave the representation unchanged for one association; exhaustive count."""
    count = 0
    for m in ([multiplicity] if multiplicity_written else list(MULTIPLICITIES)):
        for _ in product(*[(False, True)] * len(hidden_fields(m))):
            count += 1
    return count


BASE = ["N:1", "1:N", "N:1", "N:M", "1:N"]
results = {}
for written, symbols_per_assoc in ((True, 4), (False, 3)):
    total = 1
    for m in BASE:
        total *= association_options(written, m)
    results[written] = total
    print(f"multiplicity written {str(written):5s} | symbols {symbols_per_assoc * len(BASE):3d}"
          f" | same view count {total:9d}")
print("cost of removing 5 symbols: x", results[False] / results[True], sep="")
```

```
multiplicity written True  | symbols  20 | same view count      2048
multiplicity written False | symbols  15 | same view count   3200000
cost of removing 5 symbols: x1562.5
```

Five symbols are removed, and the ambiguity rises to 1562.5 times. The ratio means
more than two bits per removed symbol; yet since every hidden field is binary, at
most one bit per symbol would have been expected. The difference comes from
multiplicity **not being binary**: leaving a field that can take four values
unwritten costs two bits, and on top of that, because that field determines whether
other fields exist at all, it also makes the structure itself ambiguous.

Removing a symbol from a representation can make it more readable, but readability
is not a measured gain; **1562.5, on the other hand, is a measured cost**. Until the
two are written into the same table, simplification cannot be counted as an
improvement.

#### The Course's Rule

This lesson establishes the course's measure: **a representation's count is not the
symbols it carries, but the number of systems that collapse onto the same
representation.** A representation for which how many systems it fails to
distinguish is not written counts as unmeasured. Twenty symbols is a diagram's size;
2048 is its cost, and until the two are written together, nothing has actually been
said about the diagram.

The measure's resolution is also fixed here. The count is exhaustive, not a sample:
there is no margin of error around 2048, and when comparing two representations,
**even a difference of one system is meaningful**.

#### Summary

- A representation is an information-discarding projection of a system; there is a
  single path from the system to the representation and many paths back, and the
  loss itself is invisible on the representation.
- The maintenance workshop's five associations carry 31 facts. The twenty-symbol,
  low-detail structural representation writes 20 of these, discards 11, and
  collapses $2^{11} = 2048$ distinct systems onto a single diagram.
- A single association's three unwritten fields produce eight options, and all
  eight have exactly the same representation; whoever reads the diagram must make a
  choice and does not notice that they made one.
- The three groups of the notation family are three separate losses of the same
  system: structure discards 11 fields, behavior discards 23 of 24 traces, the data
  level discards 2 of 5 constraints, and combining all three still does not bring
  the system back.
- Adding a sixth association grows the representation by 4 symbols in every case,
  but multiplies the ambiguity by 2 or 8 depending on multiplicity: the
  representation grows linearly, the ambiguity grows exponentially.
- If multiplicity is never written, the representation drops from 20 symbols to 15,
  and the number of systems collapsing onto the same representation rises from 2048
  to 3,200,000: 1562.5 times. Simplification's gain is unmeasured; its cost is
  measured.

#### Next Step

Only the narrowest projection was measured in this lesson, and the number 2048
stands alone. Adding symbols to the diagram lowers this number — but by how much has
not been measured. Does a symbol halve the ambiguity, take more than that, or are
there symbols that take none at all.

The next lesson takes up the class diagram and counts it symbol by symbol: when the
lifecycle symbol is added, 20 symbols become 25 — to what does 2048 drop, and how
many systems remain once ordering and uniqueness notes are also written. The same
lesson will measure which fact the distinction between composition and aggregation
carries: what determines how many objects go away when a work order is deleted is a
single symbol on the diagram.

### Class Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/class-diagrams

The share of the ambiguity that relationship-type and multiplicity symbols take: 20 symbols leave 2048 systems, 25 leave 64, 31 leave 1; a binary field's symbol takes exactly one bit, while a three-valued field's symbol takes 1.58.

The previous lesson left a single number: the twenty-symbol structural
representation collapses 2048 systems onto a single diagram. The number alone says
little. Adding symbols to the diagram lowers it, but by how much has not been
measured — does a symbol halve the ambiguity, take more, or are there symbols that
take none at all.

The class representation is well suited to answering this question, because each
relationship symbol writes a specific fact, and that fact is dropped when it is not
written. What a class, inheritance, and an interface are was established in the
Programming Fundamentals course; this lesson does not define them. The only thing it
measures is: by how many times do the **relationship-type** and **multiplicity**
symbols lower the number of systems collapsing onto the same representation.

#### One Bit per Symbol

The shared definition defines three levels of detail. At level zero, only each
association's source, target, direction, and multiplicity are written. At level
one, the **lifecycle** field is also written: whether the association carries a
lifecycle ownership. At level two, the **ordered** and **unique** fields of
associations that carry a collection are also added as bracketed notes.

- **SD8** — The three levels are measured on the same system; the only thing that
  changes is the set of fields written.
- **SD9** — The bit equivalent of the ambiguity is the binary logarithm of the
  number of systems collapsing onto the same representation. If a single system
  remains, the bit count is zero and the representation has become reversible.

```python
"""M01/K06 shared definition (excerpt): the structural view at three levels of detail
and the number of systems each level collapses onto the same view. The count is
exhaustive."""
from itertools import product
from math import log2

ASSOCIATIONS = [
    {"name": "a1", "source": "WorkOrder", "target": "Technician", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a2", "source": "WorkOrder", "target": "LineItem", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": False},
    {"name": "a3", "source": "LineItem",  "target": "Part", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a4", "source": "Technician",   "target": "Machine", "direction": "two-way", "multiplicity": "N:M",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a5", "source": "WorkOrder", "target": "Document", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": True},
]


def hidden_fields(association):
    fields = ["lifecycle"]
    if association["multiplicity"] in ("1:N", "N:M"):
        fields += ["ordered", "unique"]
    return fields


def structural_view(associations, detail=0):
    symbols = []
    for assoc in associations:
        s = {"source": assoc["source"], "target": assoc["target"], "direction": assoc["direction"], "multiplicity": assoc["multiplicity"]}
        if detail >= 1:
            s["lifecycle"] = assoc["lifecycle"]
        if detail >= 2:
            for field in hidden_fields(assoc):
                s[field] = assoc[field]
        symbols.append(s)
    return symbols


def symbol_count(view):
    return sum(len(s) for s in view)


def same_view_count(associations, detail=0):
    options = {"lifecycle": ("independent", "dependent"), "ordered": (False, True),
               "unique": (False, True)}
    goal_view = structural_view(associations, detail)
    spaces = []
    for assoc in associations:
        fields = hidden_fields(assoc)
        spaces.append([dict(zip(fields, d)) for d in product(*[options[f] for f in fields])])
    count = 0
    for choice in product(*spaces):
        candidate = [dict(assoc, **c) for assoc, c in zip(associations, choice)]
        if structural_view(candidate, detail) == goal_view:
            count += 1
    return count


WRITTEN = {0: "source/target/dir/mult", 1: "+ lifecycle", 2: "+ ordered, unique"}
measurements = []
print("detail  fields written           symbols  same view    bits")
for level in (0, 1, 2):
    view = structural_view(ASSOCIATIONS, level)
    symbols, drop = symbol_count(view), same_view_count(ASSOCIATIONS, level)
    measurements.append((symbols, drop))
    print(f"  {level:5d}  {WRITTEN[level]:23s}{symbols:6d}{drop:14d}{log2(drop):7.2f}")
print()
print("transition        symbols added  bits gained  bits per symbol")
for (s0, d0), (s1, d1) in zip(measurements, measurements[1:]):
    print(f"  {s0:2d} -> {s1:2d} symbols{s1 - s0:11d}{log2(d0) - log2(d1):12.2f}"
          f"{(log2(d0) - log2(d1)) / (s1 - s0):18.2f}")
```

```
detail  fields written           symbols  same view    bits
      0  source/target/dir/mult     20          2048  11.00
      1  + lifecycle                25            64   6.00
      2  + ordered, unique          31             1   0.00

transition        symbols added  bits gained  bits per symbol
  20 -> 25 symbols          5        5.00              1.00
  25 -> 31 symbols          6        6.00              1.00
```

Three numbers side by side. **System:** five associations, 31 facts.
**Representation:** 20, 25, or 31 symbols. **Cost:** 2048, 64, and 1 system,
respectively.

The lower table gives the lesson's main result: **every added symbol takes exactly
one bit.** Five lifecycle symbols take five bits, six ordering and uniqueness notes
take six bits; neither more nor less. In the last row, the ambiguity drops to zero
bits, meaning the 31-symbol representation becomes reversible — there is a single
system that produces that representation, and the reader is not forced to make a
choice.

This exact match is not a coincidence. Every hidden field is binary: lifecycle has
two values, ordered has two values, unique has two values. Writing a binary field
cuts the option space exactly in half, and a space that is cut in half loses exactly
one bit. The next section will measure the case where this condition no longer
holds.

The measure's scope is also bounded here. The model only counts association lines;
class names, attributes, and operations are outside the model, so the number 2048 is
**only the relationship ambiguity**. A real class representation carries more than
this, and every new field it carries brings its own hidden fields with it; the
number here is a lower bound, not a total.

#### Lifecycle Ownership

The bit taken by the lifecycle symbol distinguishes two kinds of relationship on the
representation. **Composition** states a lifecycle ownership: when the owning
object is deleted, the part object is deleted too, because the part has no
existence independent of its owner. **Aggregation** does not assert ownership; the
part continues to exist after the whole is deleted. The concept of composition
itself was established in the Programming Fundamentals course; what is added here
is **which fact** the distinction between the two kinds carries.

The distinction is a single symbol on the representation; at the instance level, it
is the count of deleted objects.

- **SD10** — Example instance: one work order, three line items, two documents, one
  technician, two machines, three parts — twelve objects in total.
- **SD11** — Deletion starts from the root and propagates **transitively** only
  through dependent associations. If an association's target was not deleted while
  its source was, the target is left **unlinked**.
- **SD12** — For the valid content count of a collection, a four-part catalog and a
  three-line-item work order are taken.

```python
"""The instance-level counterpart of the unwritten fields. The lifecycle field
determines deletion propagation; the ordered and unique fields determine a
collection's valid content count. Example instance: 1 work order, 3 line items,
2 documents, 1 technician, 2 machines, 3 parts."""
from itertools import product

OBJECTS = {"WorkOrder": 1, "LineItem": 3, "Part": 3, "Technician": 1, "Machine": 2, "Document": 2}
PAIRS = [("WorkOrder", "Technician"), ("WorkOrder", "LineItem"), ("LineItem", "Part"),
         ("Technician", "Machine"), ("WorkOrder", "Document")]     # shared definition's a1..a5 order
ACTUAL = ("independent", "dependent", "independent", "independent", "dependent")


def delete(lifecycles, root="WorkOrder"):
    """Composition is lifecycle ownership: when the owner is deleted, the part is deleted too."""
    deleted, changed = {root}, True
    while changed:
        changed = False
        for (source, target), lifecycle in zip(PAIRS, lifecycles):
            if lifecycle == "dependent" and source in deleted and target not in deleted:
                deleted.add(target)
                changed = True
    unlinked = {target for (source, target), lifecycle in zip(PAIRS, lifecycles)
                if lifecycle == "independent" and source in deleted and target not in deleted}
    return sum(OBJECTS[t] for t in deleted), sum(OBJECTS[t] for t in unlinked), sorted(deleted)


distribution = {}
for lifecycles in product(("independent", "dependent"), repeat=5):
    n = delete(lifecycles)[0]
    distribution[n] = distribution.get(n, 0) + 1
print("total objects:", sum(OBJECTS.values()), "| lifecycle choices:", 2 ** 5,
      "| distinct deleted-object values:", len(distribution))
print("  deleted objects:", sorted(distribution), "-> at least", min(distribution), ", at most", max(distribution))
n, u, deleted_set = delete(ACTUAL)
print("with lifecycle written, a single value:", n, "objects deleted", deleted_set,
      "|", u, "objects left unlinked")
print()
print("a2 (WorkOrder -> LineItem, 1:N): a 4-part catalog, a 3-line-item work order")
print("  ordered  unique  valid content count")
for ordered in (False, True):
    for unique in (False, True):
        content = set()
        for d in product(range(4), repeat=3):
            if unique and len(set(d)) != len(d):
                continue
            content.add(d if ordered else tuple(sorted(d)))
        print(f"  {str(ordered):8s}{str(unique):9s}{len(content):10d}")
```

```
total objects: 12 | lifecycle choices: 32 | distinct deleted-object values: 11
  deleted objects: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] -> at least 1 , at most 12
with lifecycle written, a single value: 6 objects deleted ['Document', 'LineItem', 'WorkOrder'] | 4 objects left unlinked

a2 (WorkOrder -> LineItem, 1:N): a 4-part catalog, a 3-line-item work order
  ordered  unique  valid content count
  False   False            20
  False   True              4
  True    False            64
  True    True             24
```

Someone looking at the twenty-symbol representation cannot know how many objects
will go when a work order is deleted. Thirty-two lifecycle choices produce **eleven
distinct** deleted-object values, ranging from 1 to 12: if every association is
independent, only the work order is deleted; if all are dependent, every object in
the system goes. The diagram is consistent with all eleven of these outcomes.

There is a gap in the range, and its cause is instructive: of the twelve values,
only **11 does not appear**. Objects go in groups, not one at a time — three line
items together, three parts together, two documents together — and no combination
of these groups adds up to exactly eleven objects. Deletion behavior is not a
continuous range but a discrete set of values that the association structure
permits.

When the lifecycle symbols are written, the result drops to a single value: **6
objects** are deleted — the work order, three line items, two documents — and **4
objects are left unlinked**: the three parts the line items point to, and the
assigned technician. The five bits taken by five symbols collapse an
instance-level range to a single number. The corresponding consequence is concrete:
whoever writes the deletion procedure makes the decision themselves if that symbol
is absent from the representation, and the decision they make is not written on the
diagram.

The four objects left unlinked are a separate item, and they show the cost of
aggregation. The parts are not deleted, but there is no longer a line item pointing
to them; they sit unreachable in the system. Composition removes this situation and
replaces it with something else: every object whose owner is deleted goes, even if
it is being used from somewhere else.

The multiplicity symbol is incomplete in the same way. The representation writes
1:N between `WorkOrder` and `LineItem`, and its meaning is limited to the sentence
"a work order has more than one line item." How many distinct valid contents a
three-line-item work order can have, from a four-part catalog, depends on two
unwritten fields: **64** in an ordered list that accepts repeats, **4** in an
unordered, unique set. The sixteenfold gap between them sits as a single `1:N` on
the representation.

#### A Symbol Does Not Always Take One Bit

The result of exactly one bit per symbol came from the hidden fields being binary.
Per the shared definition's resolution rule, this condition is tested with a second
configuration: what would happen if the lifecycle field took **three** values.

- **SD13** — A three-valued lifecycle field distinguishes: a plain **association**
  that asserts no ownership, **aggregation** that shares the part, and
  **composition** that carries the part along with its lifecycle. The number of
  symbols written does not change; only the number of values the symbol can
  distinguish increases.

```python
"""Second configuration: what if the lifecycle field were three-valued instead of
binary. Three values: plain association, aggregation, composition. The number of
symbols written does not change; what changes is the number of bits a symbol takes.
The count is exhaustive again."""
from itertools import product
from math import log2

MULTIPLICITIES = ["N:1", "1:N", "N:1", "N:M", "1:N"]          # shared definition's five associations


def hidden_fields(multiplicity):
    return ["lifecycle", "ordered", "unique"] if multiplicity in ("1:N", "N:M") else ["lifecycle"]


def exhaustive_count(lifecycle_values, lifecycle_written):
    spaces = []
    for mult in MULTIPLICITIES:
        options = [range(1 if lifecycle_written else lifecycle_values) if field == "lifecycle" else range(2)
                   for field in hidden_fields(mult)]
        spaces.append(list(product(*options)))
    return sum(1 for _ in product(*spaces))


print("lifecycle values  written  symbols  same view    bits")
for values in (2, 3):
    for written, symbols in ((False, 20), (True, 25)):
        s = exhaustive_count(values, written)
        print(f"{values:17d}  {str(written):8s}{symbols:6d}{s:12d}{log2(s):8.2f}")
print()
for values in (2, 3):
    gained = log2(exhaustive_count(values, False)) - log2(exhaustive_count(values, True))
    print(f"{values}-valued lifecycle field: 5 symbols take {gained:.2f} bits"
          f", {gained / 5:.2f} per symbol")
```

```
lifecycle values  written  symbols  same view    bits
                2  False       20        2048   11.00
                2  True        25          64    6.00
                3  False       20       15552   13.92
                3  True        25          64    6.00

2-valued lifecycle field: 5 symbols take 5.00 bits, 1.00 per symbol
3-valued lifecycle field: 5 symbols take 7.92 bits, 1.58 per symbol
```

The representation is the same in both cases: 20 symbols to 25. The bits gained,
however, rise from 5.00 to 7.92, 1.58 per symbol. The rule is corrected as follows:
**a symbol takes bits equal to the binary logarithm of the number of values it
distinguishes.** For a binary field this is exactly one bit; for a three-valued
field it is 1.58, for a four-valued field it is two bits.

Reading the result in reverse is more useful. The information lost when the
lifecycle symbol is not written depends on **how many things that symbol can
distinguish**. In a representation that does not distinguish plain association from
aggregation, the unwritten symbol costs 1.00 bit; in a representation that
distinguishes all three, the same unwritten symbol costs 1.58 bits. Enriching a
representation also **makes leaving it incomplete more expensive** — and until
these two effects appear in the same table, enrichment cannot be counted as a gain.

#### Summary

- The low-detail structural representation has 20 symbols and 2048 systems; once
  lifecycle symbols are added, 25 symbols and 64 systems; once ordering and
  uniqueness notes are also added, 31 symbols and a single system remain.
- Measured in bits, every added symbol takes **exactly one bit**: 5 symbols take
  5.00 bits, 6 symbols take 6.00 bits. In the thirty-one-symbol representation, the
  ambiguity is zero bits, meaning the representation has become reversible.
- Composition states lifecycle ownership, aggregation does not. When lifecycle
  symbols are not written, deleting a work order is consistent with 11 distinct
  outcomes in the 12-object system (between 1 and 12); once written, the result
  drops to a single value: 6 objects deleted, 4 left unlinked.
- The multiplicity symbol states that a collection exists, not its content: in a
  four-part catalog, a three-line-item work order's valid content count is 64 if
  ordered and repeating, 4 if unordered and unique — a sixteenfold gap sitting as a
  single `1:N` on the representation.
- One bit per symbol holds only for binary fields. If the lifecycle field were
  three-valued, the system count collapsing onto the twenty-symbol representation
  would be 15552, and the same five symbols would take 7.92 bits, that is, 1.58
  bits per symbol.

#### Next Step

Everything counted in this lesson was static: which type is linked to what, how
many, who carries whose lifecycle. Into how many separate parts the system is split
while running was never asked — the six classes of the five associations could run
in a single unit or in six separate units, and the thirty-one-symbol representation
does not distinguish any of these.

The next lesson splits the same set of classes into runtime units and counts the
partition: into how many distinct component partitions the six classes fall, how
many of these a component representation collapses onto the same drawing, and which
associations become remote links once the components are placed onto nodes.

### Component and Deployment Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/component-and-deployment-diagrams

The same six classes fall into 203 distinct runtime partitions: a five-symbol component representation reduces 540 class distributions to 20, never states the internal-association load, and fixes the remote-link count only once deployment is written.

The previous two lessons measured the system as a static set of associations and
showed that the ambiguity drops to zero once thirty-one symbols are reached. A
reversible class representation was obtained — but that representation never says
**into how many separate parts** the six classes sit while running. All of them
could run in a single unit, or the six could run separately.

This lesson measures that gap. Classes are split into runtime units, the units are
placed onto nodes, and at each of the two steps, how many systems collapse onto the
same representation is counted. Which partition or which deployment should be
chosen is not this lesson's question; the architectural decision is the subject of
the Software Architecture curriculum and the System Design and Distributed Systems
curriculum. Only what the representations do and do not distinguish is counted
here.

#### The Class Representation Does Not State the Split

A **component** is a runtime unit made up of classes that are deployed together and
move together. For the shared definition's six classes, a component arrangement is
a **partition** of the classes: every class sits in exactly one component.

- **SD14** — The class set follows from the shared definition's associations:
  `WorkOrder`, `Technician`, `LineItem`, `Part`, `Machine`, `Document`. The
  associations are the five between a1 and a5.
- **SD15** — In a partition, if an association's two ends are in different
  components, that association **crosses out** and shows up in the component
  representation; if they are in the same component, it never shows up.
- **SD16** — A single partition is followed throughout the lesson: `Intake` = work
  order and document, `Workshop` = technician and machine, `Stock` = line item and
  part.

```python
"""M01/K06: the shared definition's association set is split into runtime units. The
class representation never writes this split; every partition of the six classes is
produced by exhaustive count."""
CLASSES = ["WorkOrder", "Technician", "LineItem", "Part", "Machine", "Document"]
PAIRS = [("WorkOrder", "Technician"), ("WorkOrder", "LineItem"), ("LineItem", "Part"),
         ("Technician", "Machine"), ("WorkOrder", "Document")]      # shared definition's a1..a5
SELECTED = [["WorkOrder", "Document"], ["Technician", "Machine"], ["LineItem", "Part"]]


def partitions(items):
    """Every partition of a set: each class in exactly one component."""
    if not items:
        yield []
        return
    first, rest = items[0], items[1:]
    for p in partitions(rest):
        for i in range(len(p)):
            yield p[:i] + [[first] + p[i]] + p[i + 1:]
        yield [[first]] + p


def external_count(partition):
    location = {c: i for i, group in enumerate(partition) for c in group}
    return sum(1 for source, target in PAIRS if location[source] != location[target])


all_partitions = list(partitions(CLASSES))
print("classes", len(CLASSES), "| associations", len(PAIRS), "| distinct component partitions", len(all_partitions))
print("selected partition:", SELECTED, "-> external associations", external_count(SELECTED))
print()
distribution = {}
for p in all_partitions:
    key = (len(p), external_count(p))
    distribution[key] = distribution.get(key, 0) + 1
print("components  external  partitions in the same rough drawing")
for key in sorted(distribution):
    marker = "  <- selected" if key == (len(SELECTED), external_count(SELECTED)) else ""
    print(f"{key[0]:8d}{key[1]:12d}{distribution[key]:22d}{marker}")
print("total:", sum(distribution.values()))
print()
EXTENDED = CLASSES + ["Supplier"]
print("class count  distinct partitions")
for n in range(4, len(EXTENDED) + 1):
    print(f"{n:12d}{len(list(partitions(EXTENDED[:n]))):18d}")
```

```
classes 6 | associations 5 | distinct component partitions 203
selected partition: [['WorkOrder', 'Document'], ['Technician', 'Machine'], ['LineItem', 'Part']] -> external associations 2

components  external  partitions in the same rough drawing
       1           0                     1
       2           1                     5
       2           2                    10
       2           3                    10
       2           4                     5
       2           5                     1
       3           2                    10  <- selected
       3           3                    30
       3           4                    35
       3           5                    15
       4           3                    10
       4           4                    30
       4           5                    25
       5           4                     5
       5           5                    10
       6           5                     1
total: 203

class count  distinct partitions
           4                15
           5                52
           6               203
           7               877
```

The six classes fall into **203 distinct partitions**, and the reversible class
representation excludes none of them. In the previous lesson, the ambiguity dropped
to zero with 31 symbols; that zero held only for the **association fields**. The
split is a different axis, and on that axis, the class representation carries no
information at all.

The table shows that even the roughest component drawing carries a number. A
representation that draws three boxes and puts two links between them is
consistent with **10** of the 203 partitions: once the box count and the count of
associations crossing out are written, 203 systems drop to 10. The same table's
most crowded row is three components and four external associations: 35
partitions. The ends of the distribution are singular — there is one partition
with a single component and it sends no association out, and one partition with
six components, sending all five associations out.

The final table gives the scale of this axis. There are 15 partitions at four
classes, 52 at five, 203 at six, 877 at seven; adding a single class to the system
increases the partition count by more than fourfold. The class representation sees
none of this growth, because the new class only adds it a few symbols. In the
previous lesson, the representation grew linearly while the association ambiguity
grew exponentially; here, the split ambiguity grows even faster than
exponentially. As a system grows, **the share of what is not said about it**
grows, and this comes from the nature of the number, not from the quality of the
representation.

#### What the Component Representation Takes

The rough drawing does not name the components. In a named representation, the
identity of the components is fixed, and what is to be measured is **which class
is in which component**.

- **SD17** — Three named components are fixed and none can be empty; so the count
  is over the **surjective** assignments of six classes onto three components.

```python
"""Named component representation: three fixed components, variable class distribution.
Internal associations do not show up in the representation; external ones do."""
from itertools import product
from math import log2

CLASSES = ["WorkOrder", "Technician", "LineItem", "Part", "Machine", "Document"]
PAIRS = [("WorkOrder", "Technician"), ("WorkOrder", "LineItem"), ("LineItem", "Part"),
         ("Technician", "Machine"), ("WorkOrder", "Document")]
COMPONENTS = ("Intake", "Workshop", "Stock")
SELECTED = {"WorkOrder": "Intake", "Document": "Intake", "Technician": "Workshop", "Machine": "Workshop",
            "LineItem": "Stock", "Part": "Stock"}


def external_structure(location):
    """The structure that shows up in the representation: how many associations between which pair of components."""
    counts = {}
    for source, target in PAIRS:
        if location[source] != location[target]:
            pair = tuple(sorted((location[source], location[target])))
            counts[pair] = counts.get(pair, 0) + 1
    return tuple(sorted(counts.items()))


def internal_load(location):
    counts = dict.fromkeys(COMPONENTS, 0)
    for source, target in PAIRS:
        if location[source] == location[target]:
            counts[location[source]] += 1
    return tuple(counts[c] for c in COMPONENTS)


surjective = [dict(zip(CLASSES, d)) for d in product(COMPONENTS, repeat=len(CLASSES))
              if len(set(d)) == len(COMPONENTS)]
goal = external_structure(SELECTED)
matching = [y for y in surjective if external_structure(y) == goal]
print("surjective assignments:", len(surjective), "| external associations of the selected distribution:",
      [(f"{a}-{b}", n) for (a, b), n in goal])
print()
print("representation level                  symbols  assignments  bits")
for label, symbols, count in (("0  boxes and their names", 3, len(surjective)),
                               ("1  + inter-component links", 5, len(matching)),
                               ("2  + each box's content", 11, 1)):
    print(f"  {label:36s}{symbols:3d}{count:12d}{log2(count):8.2f}")
print()
load = {}
for y in matching:
    load[internal_load(y)] = load.get(internal_load(y), 0) + 1
print("internal-association distribution of the", len(matching), "assignments collapsing onto the same component view")
print("  (Intake , Workshop , Stock)  assignments")
for key in sorted(load):
    print(f"  {str(key):26s}{load[key]:5d}")
```

```
surjective assignments: 540 | external associations of the selected distribution: [('Intake-Stock', 1), ('Intake-Workshop', 1)]

representation level                  symbols  assignments  bits
  0  boxes and their names              3         540    9.08
  1  + inter-component links            5          20    4.32
  2  + each box's content              11           1    0.00

internal-association distribution of the 20 assignments collapsing onto the same component view
  (Intake , Workshop , Stock)  assignments
  (0, 0, 3)                     2
  (0, 3, 0)                     2
  (1, 1, 1)                     2
  (2, 0, 1)                     4
  (2, 1, 0)                     4
  (3, 0, 0)                     6
```

Three numbers side by side. **System:** six classes, five associations, 203
partitions, and 540 surjective assignments for three named components.
**Representation:** five symbols — three boxes and two links. **Cost:** 20
assignments collapsing onto the same representation.

The steps of the ladder behave differently from the previous lesson's. The two
symbols that draw the links bring the bit count from 9.08 down to 4.32: 2.37 bits
per symbol. The six symbols that write the content take the remaining 4.32 bits:
0.72 bits per symbol. In the class representation, every symbol took exactly one
bit, because the hidden fields were independent binary choices. Here, the hidden
facts are not independent — placing one class in a component also constrains
which component the classes linked to it can be in. **Symbols that write
dependent facts do not carry equal bits.**

The lower table shows the component representation's most expensive silence. All
twenty assignments produce the same representation, but the three internal
associations that do not show up in the representation are distributed in **six
distinct ways** across these twenty assignments. At one end, all internal
associations pile up in a single component — `Intake` gets three, the others get
zero — and this happens in six assignments. At the other end, the load is split
evenly: one association per component, in two assignments. Someone looking at the
diagram sees three boxes and does not know how the load inside the boxes is
distributed; the difference between two assignments is as large as one component
carrying all the internal work versus carrying none of it. The weights of the six
distributions are not equal either: six of the twenty assignments — the table's
most crowded row — give all three internal associations to the `Intake`
component.

The reading that follows from this is: the component representation exists **to
hide internal structure**, and what it hides is not just detail, but **the
distribution of the load**. Hiding itself is not the flaw; the flaw is that it is
unmeasured.

#### Deployment and Remote Links

When components are placed onto nodes, some associations cross the node boundary.
An association that crosses the boundary is counted in the shared definition as a
**remote link**; one that does not is local.

- **SD18** — Deployment is one symbol per component: which component is on which
  node. The two inter-component links read from the component representation,
  plus the three internal associations that do not show up in it, give a total of
  five associations; internal associations are, by definition, always local.

```python
"""Deployment: components are distributed onto nodes. An association becomes REMOTE
if its two ends are on different nodes. The component representation never states
this; the deployment representation states it with one symbol per component."""
from itertools import product

COMPONENTS = ("Intake", "Workshop", "Stock")
COMPONENT_LINKS = [("Intake", "Workshop"), ("Intake", "Stock")]   # read from the component representation
INTERNAL_LINKS = 3                                                 # never shows up, always local


def remote(deployment):
    return sum(1 for a, b in COMPONENT_LINKS if deployment[a] != deployment[b])


for nodes in (2, 3):
    distribution = {}
    for combo in product(range(nodes), repeat=len(COMPONENTS)):
        distribution.setdefault(remote(dict(zip(COMPONENTS, combo))), []).append(combo)
    print(f"{nodes} nodes -> {nodes ** len(COMPONENTS)} deployments, deployment representation"
          f" {len(COMPONENTS)} symbols")
    for r in sorted(distribution):
        print(f"   remote links {r}: {len(distribution[r]):2d} deployments"
              f" | local links {INTERNAL_LINKS + len(COMPONENT_LINKS) - r}")
```

```
2 nodes -> 8 deployments, deployment representation 3 symbols
   remote links 0:  2 deployments | local links 5
   remote links 1:  4 deployments | local links 4
   remote links 2:  2 deployments | local links 3
3 nodes -> 27 deployments, deployment representation 3 symbols
   remote links 0:  3 deployments | local links 5
   remote links 1: 12 deployments | local links 4
   remote links 2: 12 deployments | local links 3
```

The component representation alone does not state the remote-link count. In a
two-node setup there are eight deployments, and these eight deployments produce
three distinct remote-link values: 0, 1, or 2. Once the three-symbol deployment
representation is added, the value drops to a single number. This is why reading a
component representation and concluding "there are two links, so there must be two
remote calls" is wrong: both links can end up on the same node, in which case the
remote-link count is zero.

The second configuration raises the node count to three. The deployment
representation is still three symbols, but the number of possible deployments
rises from 8 to 27, and the distribution shifts: the share of deployments with
zero remote links drops from two-of-eight to three-of-twenty-seven, and the share
with two remote links rises from two-of-eight to twelve-of-twenty-seven. **The
same number of symbols says less with more nodes** — because the symbol count
depends on the component count, while the ambiguity depends on the node count.

The limit of what the deployment representation distinguishes is also read from
this. The representation states whether an association is remote; it does not
state **at what speed** or **with what reliability** it operates, because the
representation has no such field. The cost of a remote link is not counted in this
course; the only thing counted here is how many associations cross the boundary,
and how much the representation determines this.

One more field sits on the representation, and it was not counted in this lesson:
the **artifact**. The deployment representation does not place components onto
nodes, it places the packaged forms of components; a component can split into
more than one artifact, and an artifact can be copied onto more than one node.
This two-way flexibility says that the three-symbol deployment representation
used here rests on the assumption of **one node per component**. Once the
assumption is lifted, the deployment count grows and the representation stays the
same — so the measured numbers 8 and 27 are lower bounds under this assumption.

#### Summary

- Six classes fall into 203 distinct component partitions, and the reversible
  class representation eliminates none of them: the split is an axis separate
  from the association fields.
- The roughest component drawing, made of three boxes and two links, is
  consistent with 10 of the 203 partitions; the same table's most crowded row is
  three components and four external associations, carrying 35 partitions.
- There are 540 surjective assignments for three named components. Three symbols
  (the boxes and their names) eliminate none of them, two link symbols bring it
  down to 20, six content symbols bring it down to a single assignment.
- Link symbols take 2.37 bits each, content symbols take 0.72 bits each; the
  result of exactly one bit per symbol from the class representation does not
  hold here, because the hidden facts are not independent of one another.
- The 20 assignments collapsing onto the same component representation distribute
  the three internal associations, invisible to the representation, in six
  distinct ways; at one end a single component carries all three, at the other
  end the load splits evenly across three.
- In a two-node setup, eight deployments give three distinct remote-link values
  (0, 1, 2); the three-symbol deployment representation fixes the value. When
  nodes rise to three, the symbol count does not change, but the deployment count
  rises to 27.

#### Next Step

All three lessons in this topic measured the system's **static** side: which type
is linked to what, how many, in which unit, on which node. All three shared the
same limit — none of them states what the system **does over time**. The
thirty-one-symbol class representation was reversible for the association fields,
but in what order a work order is processed, which states it passes through, and
which sequences never happen at all are not written on that representation.

The next topic takes up behavioral representations and builds the measure the
same way. The first lesson takes up the use case representation: it measures how
a representation that shows three of the system's external facts and
**deliberately** drops seven internal facts cannot be distinguished from one that
drops them **inadvertently**, unless that deliberate omission is put into a
number.

## Behavioral Diagrams

Counting what the use case, sequence, activity, and state machine notations carry, what they omit on purpose, and where they under-accept or over-accept.

### Use Case Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/use-case-diagrams

Counting separately what the actor, scenario, and system-boundary notation carries and what it drops: seven internal steps omitted on purpose, two links dropped by accident, and 512 systems collapsing into the same diagram.

The previous topic measured the static side of a system: which class is linked to which,
which direction a link runs, how many distinct component partitions the same class set
collapses into. Every question asked there looked at the system in its state of not
running.

This topic looks at the moment the system runs: who starts what, in what order messages
travel, in what order work can be done, what states an object passes through. The first
of the four behavioral notations carries **the least information** of the four — and
that is not a flaw. The use case notation deliberately leaves the inside of the system
empty. This lesson's job is to count that emptiness, because until it is counted, there
is no way to know whether a gap was **left on purpose** or **fell through unnoticed**.

The lesson's question is not which diagram type fits which question; that measurement
was made over a set of eighteen questions in the Architectural Decisions and
Documentation course of the Software Architecture curriculum, and is not repeated here.
The question here begins **after** a type has been chosen: how many distinct systems
does the chosen type fail to tell apart.

#### The Four Fields the Notation Carries

The use case notation carries four kinds of information: the **actor**, the
**scenario**, the **link** connecting an actor to a scenario, and the **system
boundary**, which states what is the system's own work and what belongs to a neighbor.
These are the fields this measurement counts. The notation also defines inclusion,
extension, and generalization relationships between scenarios — inclusion is used further
below — but none of them carries order, condition, internal step, or data.

In the repair-shop example, these four fields correspond to the following. There are
two actors: the customer and the intake clerk. There are three external facts: opening
a work order, checking status, picking up. All three are started from outside the
system, and all three are the workshop's own work — that is what the boundary states,
and these three ownership decisions are three separate facts. Inside the system, by
contrast, seven internal steps run — allocating stock, assigning a technician,
reserving a workbench, processing a line item, generating a document, computing cost,
keeping records — and none of them is started by an actor.

Total: **20 facts** — 2 actors, 3 scenarios, 5 actor–scenario links, 3 ownerships, 7
internal steps.

#### Building and Counting the Diagram

The model below does not draw a diagram; it builds the **list of fields the diagram
carries** and finds, by exhaustive count, how many distinct systems produce that same
list. The real set of links has five links (**DD1**); the drawn diagram writes three of
them (**DD2**).

```python
"""Use case notation: intentionally omitted facts and inadvertently dropped
facts are counted separately. The count is exhaustive, not an estimate."""
from itertools import product

INTERNAL_FACTS = ["allocating stock", "assigning a technician", "reserving a workbench",
                   "processing a line item", "generating a document", "computing cost",
                   "keeping records"]
EXTERNAL_FACTS = ["opening a work order", "checking status", "picking up"]

# DD1: the workshop's real actor-scenario links and each scenario's owner
ACTORS = ["Customer", "IntakeClerk"]
REAL_LINKS = [("Customer", "opening a work order"), ("Customer", "checking status"),
              ("Customer", "picking up"), ("IntakeClerk", "opening a work order"),
              ("IntakeClerk", "picking up")]
REAL_OWNER = {s: "system" for s in EXTERNAL_FACTS}   # all three are the workshop's own work
# DD2: the drawn diagram writes three of the five links
DRAWN = [("Customer", "opening a work order"), ("Customer", "checking status"),
         ("Customer", "picking up")]


def notation(links, internal, owner, drawn_links, drawn_internal=(), boundary=True):
    """The notation carries only this: actor, scenario, actor-scenario link,
    system boundary. An undrawn link or internal step never appears in the
    notation; if the boundary is not drawn, whether a scenario is the
    system's own work or a neighbor's is never stated."""
    return (tuple(ACTORS),
            tuple(EXTERNAL_FACTS) + tuple(sorted(i for i in internal if i in drawn_internal)),
            tuple(sorted(b for b in links if b in drawn_links)),
            tuple(owner[s] for s in EXTERNAL_FACTS) if boundary else None)


def symbol_count(drawn_links, drawn_internal=(), boundary=True):
    return (len(ACTORS) + len(EXTERNAL_FACTS) + len(drawn_links) + (1 if boundary else 0)
            + 2 * len(drawn_internal))


def systems_collapsed(drawn_links, drawn_internal=(), boundary=True):
    """How many distinct systems produce the same diagram: exhaustive count."""
    target = notation(REAL_LINKS, INTERNAL_FACTS, REAL_OWNER, drawn_links,
                       drawn_internal, boundary)
    count = 0
    for link_mask in product((0, 1), repeat=len(REAL_LINKS)):
        links = [b for b, m in zip(REAL_LINKS, link_mask) if m]
        for internal_mask in product((0, 1), repeat=len(INTERNAL_FACTS)):
            internal = [i for i, m in zip(INTERNAL_FACTS, internal_mask) if m]
            for owner_mask in product(("system", "neighbor"), repeat=len(EXTERNAL_FACTS)):
                owner = dict(zip(EXTERNAL_FACTS, owner_mask))
                if notation(links, internal, owner, drawn_links, drawn_internal, boundary) == target:
                    count += 1
    return count


total_facts = (len(ACTORS) + len(EXTERNAL_FACTS) + len(REAL_LINKS) + len(EXTERNAL_FACTS)
               + len(INTERNAL_FACTS))
print("SYSTEM  :", total_facts, "facts =", len(ACTORS), "actors +", len(EXTERNAL_FACTS),
      "scenarios +", len(REAL_LINKS), "links +", len(EXTERNAL_FACTS), "ownerships +",
      len(INTERNAL_FACTS), "internal steps")
print("NOTATION:", symbol_count(DRAWN), "symbols")
print("COST    :", systems_collapsed(DRAWN), "systems collapse into the same diagram")
print()
print("  intentionally omitted (notation cannot carry) :", len(INTERNAL_FACTS), "internal steps")
print("  inadvertently dropped (could have been carried):",
      len([b for b in REAL_LINKS if b not in DRAWN]), "links")
for b in REAL_LINKS:
    if b not in DRAWN:
        print("     not drawn:", b[0], "->", b[1])
print("  drawn diagram             : symbols", symbol_count(DRAWN),
      "| collapses to", systems_collapsed(DRAWN))
print("  all five links drawn      : symbols", symbol_count(REAL_LINKS),
      "| collapses to", systems_collapsed(REAL_LINKS), "<- BASELINE of the notation")
print()
print("what happens to the baseline when a field is dropped")
print("field set                    symbols  collapse  bits carried")
BASELINE = systems_collapsed(REAL_LINKS)
for name, links, boundary in (("actor+scenario+link+boundary", REAL_LINKS, True),
                               ("link dropped", [], True),
                               ("boundary dropped", REAL_LINKS, False),
                               ("both dropped", [], False)):
    d = systems_collapsed(links, (), boundary)
    print(f"{name:29s} {symbol_count(links, (), boundary):7d} {d:9d} {(d // BASELINE).bit_length() - 1:14d}")
print()
# DD3: second configuration. Two internal steps are drawn as an "included use case."
EXTRA = ("generating a document", "computing cost")
print("if two internal steps are drawn as an included use case:")
print("  symbols", symbol_count(REAL_LINKS), "->", symbol_count(REAL_LINKS, EXTRA),
      "| collapse", BASELINE, "->", systems_collapsed(REAL_LINKS, EXTRA))
print("  cost: number of actorless scenarios 0 ->", len(EXTRA),
      "| external-fact ratio", f"{len(EXTERNAL_FACTS)}/{len(EXTERNAL_FACTS)}",
      "->", f"{len(EXTERNAL_FACTS)}/{len(EXTERNAL_FACTS) + len(EXTRA)}")

# DD4: if the two actors are merged into a single role
POSSIBLE = [(a, s) for a in ACTORS for s in EXTERNAL_FACTS]


def merged(links, internal, owner):
    """Single-actor diagram: only 'is some actor linked' per scenario."""
    return (("User",), tuple(EXTERNAL_FACTS),
            tuple(any(b[1] == s for b in links) for s in EXTERNAL_FACTS),
            tuple(owner[s] for s in EXTERNAL_FACTS))


target = merged(REAL_LINKS, INTERNAL_FACTS, REAL_OWNER)
count = 0
for link_mask in product((0, 1), repeat=len(POSSIBLE)):
    links = [b for b, m in zip(POSSIBLE, link_mask) if m]
    for internal_mask in product((0, 1), repeat=len(INTERNAL_FACTS)):
        internal = [i for i, m in zip(INTERNAL_FACTS, internal_mask) if m]
        if merged(links, internal, REAL_OWNER) == target:
            count += 1
print()
print("if the two actors are merged into a single role:")
print("  symbols", symbol_count(REAL_LINKS), "-> 8 | collapse", BASELINE, "->", count,
      "| ratio", count // BASELINE, "x")
```

```
SYSTEM  : 20 facts = 2 actors + 3 scenarios + 5 links + 3 ownerships + 7 internal steps
NOTATION: 9 symbols
COST    : 512 systems collapse into the same diagram

  intentionally omitted (notation cannot carry) : 7 internal steps
  inadvertently dropped (could have been carried): 2 links
     not drawn: IntakeClerk -> opening a work order
     not drawn: IntakeClerk -> picking up
  drawn diagram             : symbols 9 | collapses to 512
  all five links drawn      : symbols 11 | collapses to 128 <- BASELINE of the notation

what happens to the baseline when a field is dropped
field set                    symbols  collapse  bits carried
actor+scenario+link+boundary       11       128              0
link dropped                        6      4096              5
boundary dropped                   10      1024              3
both dropped                        5     32768              8

if two internal steps are drawn as an included use case:
  symbols 11 -> 15 | collapse 128 -> 32
  cost: number of actorless scenarios 0 -> 2 | external-fact ratio 3/3 -> 3/5

if the two actors are merged into a single role:
  symbols 11 -> 8 | collapse 128 -> 3456 | ratio 27 x
```

Three numbers stand side by side: the system has **20 facts**, the notation has **9
symbols**, the cost is **512 systems**. Five hundred twelve distinct repair shops look
identical through this diagram; someone looking at the diagram cannot know which one
they are looking at.

#### Two Distinct Losses

The number five hundred twelve comes from two different sources, and they are useless
unless kept apart.

**Intentionally omitted facts.** None of the seven internal steps find a place in the
notation's information set. The use case notation has no field for writing an internal
step; even if it did, it would not be written, because this notation's reason for
existing is to close off the system's inside and show its outside. This loss **binds
the type, not the drafter**: every use case diagram drops these seven facts, no matter
how carefully it is drawn.

**Inadvertently dropped facts.** The intake clerk's two links were of a kind the
notation could carry — an actor–scenario link, the third item on the list. They were
not written. This loss **binds the drafter, not the type**, and can be fixed
without drawing a different diagram.

The difference between the two cannot be seen by looking at the diagram. The reader sees
the same thing in both cases: information that is not there. Whether a piece of
information is missing **on purpose or by carelessness** can only be shown by holding
the system itself and comparing it against the notation. This is why a loss cannot be
known as intentional until it is counted; an uncounted gap is not a defended gap, only
an unseen one.

#### Baseline Ambiguity Comes with the Type

When the two links are written, the ambiguity drops from 512 to **128** — a factor of
four. The entire drop is credited to the drafter, because the price paid is only two
symbols — 9 symbols becomes 11.

The number 128 does not go any lower from there. This is the use case notation's
**baseline ambiguity**: each of the seven internal steps either exists in the system or
does not, the notation writes none of them, and so $2^7 = 128$ distinct systems collapse
into the same diagram. The baseline is a number that does not change with drafting
skill; **it comes with the type**.

There are two numbers to look at when evaluating a notation. The baseline states how
much of the chosen type refuses, from the start, to show. The gap between the baseline
and the actual ambiguity states how much the drafter lost unnecessarily. The first is a
design decision, the second is a defect, and they cannot be told apart when referred to
by a single number.

#### A Symbol Is Not Always One Bit

The middle table drops the fields one at a time and measures where the baseline lands.
Dropping the five link symbols sends the baseline from 128 to 4096: five symbols were
carrying **5 bits**. Exactly one bit per link, because each link either exists in the
system or does not.

The boundary does not behave this way. Dropping the single boundary symbol sends the
baseline from 128 to **1024** — one symbol was carrying **3 bits**. The reason: the
boundary does not state a single fact but the ownership of all three scenarios at once.
In a diagram where the boundary is not drawn, the reader cannot know whether opening a
work order is the workshop's own work or a neighboring system's; three unknowns remain
for three scenarios.

This differs from the ratio measured in the structural notation, where every added
symbol carried exactly one bit of ambiguity, because every symbol there wrote a single
binary field. In the use case notation, the number of bits a symbol carries depends on
how many facts that symbol fixes at once. When both are dropped, the baseline rises to
32768 and the notation drops to 5 symbols: what remains is nothing but two actor names
and three scenario names, and **8 bits** of information have been lost. The rule that
follows is that counting symbols alone is not a measure by itself; what should be
counted is not the symbol but **the fact the symbol fixes**.

#### The Count of Merging Roles

The same measure also works in the opposite direction. If the customer and the intake
clerk are merged into a single user role (**DD4**), the diagram simplifies: 11 symbols
becomes 8, the link count drops from five to three, the drawing shrinks. Ambiguity, on
the other hand, rises from 128 to **3456** — a factor of **27**.

The number twenty-seven can be read directly. The single-actor diagram states, for each
scenario, only that "some user is linked to it," not which role is linked. For each of
the three scenarios, at least one of the two roles has to be linked, which gives $2^2 -
1 = 3$ possibilities per scenario, and $3^3 = 27$ for three scenarios. Multiplied by the
128 of the seven internal steps, this gives 3456.

Here the justification for keeping the roles separate is not a matter of style but a
measured quantity of information. Three symbols were saved, and roughly 4.75 bits of
information were given up in exchange. The question to ask when simplifying a notation
is not whether the drawing looks cleaner, but which facts have become indistinguishable.

#### Bringing an Internal Step into the Notation

There is exactly one way to bring the ambiguity below the baseline: make the
untransportable fact transportable. Generating a document and computing cost can be
drawn as second-level scenarios included inside the scenario (**DD3**). The number drops
immediately: the baseline goes from **128 to 32**, two bits are gained, and the notation
rises from 11 symbols to 15.

The cost is not the symbol count. The two newly drawn scenarios have **no actor link at
all** — no actor starts them, because both are the system's own work. The number of
actorless scenarios rises from 0 to 2. The ratio of external facts in the diagram drops
from 3/3 to 3/5: only three of the diagram's five scenarios are now started from outside
the system.

What this means is that the system boundary no longer separates anything. What the
boundary carried was the information "the inside is closed, the outside enters through
these three doors"; once the internal steps move to the same side of the door, that
information becomes void. Ambiguity dropped by a factor of four, but **what the notation
says changed** — it no longer describes the face visible from outside, but part of the
work itself. The gain is 2 bits and can be counted; the loss is the meaning of a field,
and it can only be counted once it is noticed.

#### Summary

- The use case notation carries four fields: actor, scenario, actor–scenario link,
  system boundary. Order, condition, internal step, and data are not in this list.
- Of the workshop's 20 facts (2 actors, 3 scenarios, 5 links, 3 ownerships, 7 internal
  steps), the drawn diagram writes 9 symbols, and **512 distinct systems** collapse into
  the same diagram.
- Five hundred twelve comes from two sources: **7 internal steps** the notation cannot
  carry, and **2 links** it could have carried but that were not drawn. The two cannot
  be told apart by looking at the diagram.
- Once the two missing links are drawn, ambiguity drops to 128; 128 is the notation's
  **baseline ambiguity**, it comes with the type, and it does not change with drafting
  skill.
- The five link symbols carry 5 bits, the single boundary symbol carries 3 bits: how
  many bits a symbol carries depends on how many facts that symbol fixes at once.
- Merging two actors into a single role brings 11 symbols down to 8 but raises
  ambiguity from 128 to 3456: a factor of 27. Drawing two internal steps as an included
  scenario brings the baseline down to 32 but raises the number of actorless scenarios
  from 0 to 2 and drops the external-fact ratio from 3/3 to 3/5.

#### Next Step

The use case notation deliberately closed off the inside of the system, and the cost of
that was counted. The next notation does the opposite: it lifts the lid and shows the
inside, laying messages out along a time axis. At first glance this looks like a choice
that zeroes out the loss — every message, every participant, every sequence number is
written.

The next lesson measures why this is a misreading. The workshop's flow, because of the
two branches of allocation, the two branches of finding a technician, the variable
number of line items, and optional document generation, produces **24 distinct traces**.
A sequence diagram is, by definition, **a single trace**; its coverage is 1/24, that is,
0.0417. Four diagrams make 0.1667. The lesson counts where the alternative-fragment and
loop-fragment notations move this ratio, and at what cost raising the ratio to 1.0
comes.

### Sequence Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/sequence-diagrams

Measuring the trace coverage of a diagram that shows message exchange along a time axis: 0.0417 for one of 24 traces, 0.1667 for four, 1.0 with alternative and loop fragments — and 24 cases of over-approximation if the loop bound is left unwritten.

The previous lesson measured a notation that deliberately closed off the inside of the
system: seven internal steps never appeared in the notation, and 128 distinct systems
collapsed into the same diagram. This lesson opens the inside up. The sequence notation
lines participants up side by side, writes messages as arrows between them, and orders
the arrows from top to bottom. Every message is written together with its sender and
receiver; what carries the order is the arrow's vertical position, and a sequence number
is an optional addition.

At first glance this looks like the place where the loss closes up. It does not. The
sequence notation closes one loss at the cost of another: while it shows **one run** of
the system in full, it never shows **every other run** of the system at all. This
lesson's measure is the count of that "other."

#### The Traces the System Produces

The repair shop's flow branches at four points. Allocating stock can succeed, or it can
fail and the work is held and retried. A suitable technician can be found, or none is
found and the work is queued. A work order can carry one, two, or three line items.
Document generation can be requested or not.

Because these four dimensions are independent, the system produces $2 \times 2 \times 3
\times 2 = 24$ distinct **traces**. A trace is the message sequence of one run from
start to finish; their lengths range from six to twelve. A sequence diagram, by
definition, draws **one** of these 24 sequences.

#### Measuring Coverage

The model below builds the shared reference's flow and trace definitions exactly as
they are, then adds the concept of a **fragmented diagram** on top (**DD5**): if a
dimension is left "open," the diagram draws that dimension not as a single fixed value
but as an alternative or loop fragment, and covers every value of that dimension. The
symbol count is the sum of participants, messages, fragments, and guard conditions.

```python
"""Sequence notation: how many traces a diagram covers, how many it leaves
out. The FLOW and traces() definitions from the shared reference are built
exactly as they are there."""
from itertools import product

FLOW = {
    "start": [("Intake", "WorkOrder", "create"), ("WorkOrder", "Stock", "allocate")],
    "allocation_succeeded": [("WorkOrder", "Workshop", "schedule")],
    "allocation_failed": [("WorkOrder", "Intake", "hold"), ("WorkOrder", "Stock", "allocate")],
    "technician_found": [("Workshop", "Technician", "assign")],
    "no_technician": [("Workshop", "Intake", "queue"), ("Workshop", "Technician", "assign")],
    "each_item": [("Technician", "Item", "process")],
    "document": [("WorkOrder", "Document", "generate")],
    "finish": [("WorkOrder", "Intake", "close")],
}
ITEM_RANGE = (1, 2, 3)


def build_trace(allocation, technician, k, document):
    trace = list(FLOW["start"])
    if allocation == "failed":
        trace += FLOW["allocation_failed"]
    trace += FLOW["allocation_succeeded"]
    trace += FLOW["technician_found"] if technician == "found" else FLOW["no_technician"]
    trace += FLOW["each_item"] * k
    if document:
        trace += FLOW["document"]
    trace += FLOW["finish"]
    return tuple(trace)


def traces(item_range=ITEM_RANGE):
    return [build_trace(a, u, k, b)
            for a in ("succeeded", "failed") for u in ("found", "none")
            for k in item_range for b in (True, False)]


def coverage(diagrams, all_traces):
    covered = {g for g in diagrams if g in all_traces}
    return {"diagrams": len(diagrams), "covered": len(covered),
            "total": len(all_traces), "ratio": round(len(covered) / len(all_traces), 4)}


# ---- DD5: fragmented diagram. An open dimension is drawn with a fragment
# symbol; a closed dimension is fixed to a single value. Symbol count:
# participants + messages + fragments + guards.
PARTICIPANTS = 7   # Intake, WorkOrder, Stock, Workshop, Technician, Item, Document


def fragmented(open_dims, fixed=("succeeded", "found", 3, True), item_max=3):
    allocation, technician, k, document = fixed
    messages = len(FLOW["start"]) + len(FLOW["allocation_succeeded"]) + len(FLOW["finish"])
    fragments = guards = 0
    if "allocation" in open_dims:
        messages += len(FLOW["allocation_failed"]); fragments += 1; guards += 2
    elif allocation == "failed":
        messages += len(FLOW["allocation_failed"])
    if "technician" in open_dims:
        messages += len(FLOW["technician_found"]) + len(FLOW["no_technician"]); fragments += 1; guards += 2
    else:
        messages += len(FLOW["technician_found"] if technician == "found" else FLOW["no_technician"])
    if "item" in open_dims:
        messages += 1; fragments += 1; guards += 1
    else:
        messages += k
    if "document" in open_dims:
        messages += 1; fragments += 1; guards += 1
    elif document:
        messages += 1
    accepted = [build_trace(a, u, kk, b)
                for a in (("succeeded", "failed") if "allocation" in open_dims else (allocation,))
                for u in (("found", "none") if "technician" in open_dims else (technician,))
                for kk in (range(1, item_max + 1) if "item" in open_dims else (k,))
                for b in ((True, False) if "document" in open_dims else (document,))]
    return {"symbols": PARTICIPANTS + messages + fragments + guards, "messages": messages,
            "fragments": fragments, "accepted": accepted}


def flat_symbols(trace):
    """The symbol count of an unfragmented sequence diagram: participants + drawn messages."""
    return PARTICIPANTS + len(trace)


TRACES = traces()
print("SYSTEM  :", len(TRACES), "traces | lengths:", sorted({len(i) for i in TRACES}))
print("NOTATION:", fragmented(())["symbols"], "symbols (unfragmented, single trace)")
print("COST    :", len(TRACES) - 1, "traces never enter the notation")
print()
for n in (1, 2, 3, 4):
    k = coverage([tuple(i) for i in TRACES[:n]], TRACES)
    print(f"  {n} unfragmented diagram(s) -> coverage {k['covered']}/{k['total']}"
          f" = {k['ratio']} | symbols {sum(flat_symbols(i) for i in TRACES[:n])}")
print()
print("as fragments are added")
print(f"{'open dimension':36s} symbols  covered  coverage  symbols/trace")
accumulated = ()
for dim in ("item", "document", "allocation", "technician"):
    accumulated += (dim,)
    p = fragmented(accumulated)
    cov = coverage(p["accepted"], TRACES)
    print(f"{'+'.join(accumulated):36s} {p['symbols']:5d} {cov['covered']:9d}"
          f"  {cov['ratio']:6} {p['symbols'] / cov['covered']:9.2f}")
print()
print("covering all 24 traces unfragmented:", sum(flat_symbols(i) for i in TRACES),
      "symbols | covering with fragments:",
      fragmented(("item", "document", "allocation", "technician"))["symbols"], "symbols")
print()
# DD6: if the loop bound [1..*] is written. Comparison window item <= 6.
open_dims = ("item", "document", "allocation", "technician")
for limit, label in ((3, "loop bound [1..3]"), (6, "loop bound [1..*]")):
    p = fragmented(open_dims, item_max=limit)
    cov = coverage(p["accepted"], TRACES)
    over = [i for i in p["accepted"] if i not in TRACES]
    print(f"{label:22s} accepts {len(p['accepted']):3d} | coverage"
          f" {cov['covered']}/{cov['total']} = {cov['ratio']} | over-approximation {len(over)}")
print()
# DD7: if the TRACE SET grew without the system changing. A workshop with an item range of 1..6.
TRACES6 = traces((1, 2, 3, 4, 5, 6))
single = [build_trace("succeeded", "found", 3, True)]
p3 = fragmented(open_dims, item_max=3)
print("the same two diagrams, if the trace set were", len(TRACES), "instead of", len(TRACES6))
for label, g in (("unfragmented single trace", single), ("four-fragment diagram", p3["accepted"])):
    a, b = coverage(g, TRACES), coverage(g, TRACES6)
    print(f"  {label:22s} coverage {a['covered']}/{a['total']} = {a['ratio']}"
          f"  ->  {b['covered']}/{b['total']} = {b['ratio']}")
print()
# DD8: every message kind is synchronous or asynchronous. How many systems
# collapse if the notation does not write it.
MESSAGE_KINDS = sorted({m[2] for a in FLOW.values() for m in a})
REAL_MODE = {x: ("asynchronous" if x in ("generate", "queue") else "synchronous")
             for x in MESSAGE_KINDS}


def mode_notation(mode, written):
    return tuple(mode[x] if x in written else None for x in MESSAGE_KINDS)


def modes_collapsed(written):
    target = mode_notation(REAL_MODE, written)
    return sum(1 for s in product(("synchronous", "asynchronous"), repeat=len(MESSAGE_KINDS))
               if mode_notation(dict(zip(MESSAGE_KINDS, s)), written) == target)


full = fragmented(open_dims, item_max=3)["symbols"]
print("the diagram with coverage 1.0 has", len(MESSAGE_KINDS), "message kinds")
for label, written in (("mode never written", ()),
                       ("only the two asynchronous modes written", ("generate", "queue")),
                       ("all nine of the nine modes written", tuple(MESSAGE_KINDS))):
    print(f"  {label:34s} symbols {full + len(written):3d}"
          f" | collapse into the same notation {modes_collapsed(written):4d}")
```

```
SYSTEM  : 24 traces | lengths: [6, 7, 8, 9, 10, 11, 12]
NOTATION: 16 symbols (unfragmented, single trace)
COST    : 23 traces never enter the notation

  1 unfragmented diagram(s) -> coverage 1/24 = 0.0417 | symbols 14
  2 unfragmented diagram(s) -> coverage 2/24 = 0.0833 | symbols 27
  3 unfragmented diagram(s) -> coverage 3/24 = 0.125 | symbols 42
  4 unfragmented diagram(s) -> coverage 4/24 = 0.1667 | symbols 56

as fragments are added
open dimension                       symbols  covered  coverage  symbols/trace
item                                    16         3   0.125      5.33
item+document                           18         6    0.25      3.00
item+document+allocation                23        12     0.5      1.92
item+document+allocation+technician     28        24     1.0      1.17

covering all 24 traces unfragmented: 384 symbols | covering with fragments: 28 symbols

loop bound [1..3]      accepts  24 | coverage 24/24 = 1.0 | over-approximation 0
loop bound [1..*]      accepts  48 | coverage 24/24 = 1.0 | over-approximation 24

the same two diagrams, if the trace set were 24 instead of 48
  unfragmented single trace coverage 1/24 = 0.0417  ->  1/48 = 0.0208
  four-fragment diagram  coverage 24/24 = 1.0  ->  24/48 = 0.5

the diagram with coverage 1.0 has 9 message kinds
  mode never written                 symbols  28 | collapse into the same notation  512
  only the two asynchronous modes written symbols  30 | collapse into the same notation  128
  all nine of the nine modes written symbols  37 | collapse into the same notation    1
```

Three numbers: the system has **24 traces**, the notation has **16 symbols**, the cost
is **23 traces** that never enter the notation. Coverage is 1/24, that is, 0.0417.

#### Four Diagrams Are Not Enough

Coverage grows linearly. Two diagrams give 2/24 = 0.0833, three give 3/24 = 0.125, four
give 4/24 = 0.1667. Every new diagram adds exactly one trace, because each is a single
run and runs do not substitute for one another.

Covering all twenty-four traces with unfragmented diagrams requires **384 symbols**.
This is the total message count plus the participant list repeated twenty-four times.
No one reads a document like that; this is why sequence diagrams stay, in practice,
limited to three or five of them, and those three or five mean a coverage of around
0.125.

The sentence that follows is this: **a set of sequence diagrams does not show the
system, it shows a few examples from the system.** Which examples are chosen is a
choice, and that choice is not written in the notation — a reader holding four traces
cannot read from the diagram which four of the 24 they are.

#### Fragment Symbols Grow Coverage Per Symbol

The middle table measures another path. When the loop fragment is opened, the item
dimension is not fixed to a single value: instead of three messages, one message and one
loop fragment are drawn. The symbol count stays at 16 — **the same** as the unfragmented
diagram — but the covered traces rise from 1 to 3.

The next three rows proceed the same way. Once optional document generation is placed
in a fragment, 18 symbols cover 6 traces. Once the two branches of allocation are placed
in an alternative fragment, 23 symbols cover 12. Once the two branches of finding a
technician are added as well, 28 symbols cover all 24 traces. Symbols per trace drop
from 5.33 to **1.17**; achieving the same coverage unfragmented would need 384 symbols,
while the fragmented notation finishes with **28 symbols**, roughly fourteen times
fewer.

This does not mean the fragmented notation is free. An unfragmented diagram is a single
top-to-bottom reading; a fragmented diagram asks the reader to make a choice at each of
the four fragments, and the number of readings rises to 24. Coverage was gained,
**single readability** was given up. What the notation states also changed: an
unfragmented diagram says "such a run occurred"; a fragmented diagram says "these runs
can occur." The first is an observation, the second a claim, and the claim can be
wrong.

#### Coverage Is Not a Quality Measure

The bottom two blocks give two separate proofs of this.

First: if no upper bound is written on the loop fragment (**DD6**) — that is, if "one or
more items" is stated without an upper bound — coverage stays at 24/24 = 1.0, but the
number of sequences the diagram accepts rises from 24 to 48. Twenty-four are real,
twenty-four are sequences that do not exist in the system: work orders with four, five,
or six items. **Over-approximation rose from 0 to 24, and coverage did not change at
all.** Coverage answers only the question "how many of the real traces were shown"; it
does not answer "how many of the shown traces are real," and a notation is not
considered measured until the second question is asked as well.

Second: as the trace set grows, the ratio drops with the notation completely unchanged.
In a workshop where the item range is one to six instead of three (**DD7**), the same
system produces 48 traces. The unfragmented single diagram's coverage drops from 0.0417
to **0.0208**, the four-fragment diagram's coverage from 1.0 to **0.5**. Not a single
symbol was added to the diagrams, not a single symbol was removed.

For this reason a coverage ratio is never written alone anywhere. "Coverage 0.5" carries
no information; "24 of 48 traces" does. The ratio is a number that depends on the size
of the denominator, and comparison is impossible when the denominator is not written.

#### A Covered Trace Can Also Be Written Incompletely

Coverage measures whether a trace **was drawn**, not **how it was drawn**. The final
block counts this second axis. The sequence notation can write every message with a
mode: synchronous if the caller waits for a reply, asynchronous if it does not. This is
a binary decision for each of the flow's nine distinct message kinds, and in the
workshop two of them are asynchronous — document generation and queuing (**DD8**).

The 28-symbol diagram with coverage 1.0 writes none of these nine decisions. The result:
**512 distinct systems** collapse into the same diagram. The diagram covers every trace
and still does not say which system it shows. Once the two asynchronous modes are
written, the number drops to 128; once all nine are written, it drops to **1** — the
cost is going from 28 symbols to 37.

The two axes are independent, and a notation is not considered measured until both are
written separately. The first is the question of **which traces**, and coverage is its
answer. The second is the question of **how much of each trace**, and the number of
systems collapsing into the same notation is its answer. A notation can score perfectly
on the first and stay at 512 on the second; the reverse also happens — a diagram that
writes a single trace with every one of its fields is 1 on the second axis and 0.0417
on the first.

#### Summary

- The repair shop's flow produces **24 traces** through four independent branch points; a
  sequence diagram, by definition, draws **one** of these traces, giving a coverage of
  1/24 = 0.0417 and leaving 23 traces out.
- Coverage grows linearly with the number of unfragmented diagrams: four diagrams give
  4/24 = 0.1667 and 56 symbols. Covering all 24 traces unfragmented requires **384
  symbols**.
- Loop and alternative fragments grow coverage per symbol: 16 symbols for 3 traces, 18
  for 6, 23 for 12, 28 for 24. Symbols per trace drop from 5.33 to 1.17.
- The fragmented notation trades away single readability: four fragments ask the reader
  to make four choices, and the number of possible readings rises to 24. The notation
  stops being an observation and becomes a claim.
- Coverage of 1.0 is not a measure of success: when the loop bound is left unwritten,
  coverage stays at 1.0 while over-approximation rises from 0 to **24**; when the trace
  set grows from 24 to 48, the same diagrams' coverage drops from 0.0417 to 0.0208 and
  from 1.0 to 0.5.
- Coverage and ambiguity are separate axes: the 28-symbol diagram with coverage 1.0
  cannot distinguish **512 systems** because it does not write the mode of nine
  messages; once all nine are written, it drops to 1 with 37 symbols.

#### Next Step

The sequence notation never asked one question: could these messages have gone in
**another order**. Every diagram writes a single order and shows that order as if it
were mandatory. Yet most of the work in the shop does not wait on the rest; there is no
priority between informing the customer and computing the cost, and the two can be done
in either order.

The next notation writes exactly this: it writes the **prerequisites** between tasks,
not the order. Ten tasks and thirteen prerequisites determine which orders are valid,
and the number of such orders is how many distinct outputs the topological sort from
the Data Structures course can produce. The number is **168**. An activity diagram does
not accept a single execution order but all 168 execution orders at once — and unless
the number of that looseness is written, the reader mistakes it for certainty.

### Activity Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/activity-diagrams

Counting, with topological sort, how many total orderings the diagram accepts when it shows a workflow through prerequisites: ten tasks, thirteen prerequisites, 168 total orderings, a six-layer critical path, and where adding one prerequisite sends the 168.

The previous lesson counted how the sequence notation shows one run in full and the
other 23 runs not at all. Every message that notation wrote stood in a definite order,
and that order looked as if it were mandatory. Yet most of the work in the shop does
not wait on the rest: there is no priority between informing the customer and computing
the cost, and the two can be done in either order.

The activity notation corrects this. It does not write order, it writes
**prerequisites**: it states which task can start after which tasks, not which one is
done first. This frees the notation from telling a lie. In exchange, it does something
else: it describes **not a single execution, but a set of executions** at once. This
lesson's measure is the size of that set.

#### A Partial Order Describes a Set

The shop's workflow contains ten tasks: intake, allocating stock, assigning a
technician, reserving a workbench, informing the customer, computing cost, processing,
documenting, issuing an invoice, closing. There are thirteen prerequisites among them.
These thirteen prerequisites establish a **partial order**: they fix the order of some
task pairs and leave others free.

What the system actually does, however, is a **total order**. While a work order runs,
tasks happen one at a time and fall into a specific order; that run has exactly one
order. The notation, on the other hand, accepts all of these orders at once. The number
of accepted orders is the measure of the looseness the notation leaves.

This number is how many distinct outputs the **topological sort** from the Data
Structures course can produce. That lesson built the in-degree-based procedure and
showed that the ordering is not unique — that the algorithm makes a choice whenever more
than one vertex is in the queue — but it did not count how many distinct **results**
those choices produce. This lesson does that count. The procedure is not rewritten; it
is called exactly as it is.

#### The Number Itself

```python
"""Activity notation: how many total orderings a partial order accepts at
once. The ACTIVITY and total_orderings() definitions from the shared
reference are built exactly as they are there; the topological sort is the
in-degree-based procedure from the Data Structures course."""
from collections import deque
from itertools import combinations

ACTIVITY = {                      # task -> prerequisite tasks
    "intake": [],
    "allocate_stock": ["intake"],
    "assign_technician": ["intake"],
    "reserve_workbench": ["assign_technician"],
    "inform_customer": ["intake"],
    "compute_cost": ["allocate_stock"],
    "process": ["allocate_stock", "reserve_workbench"],
    "document": ["process"],
    "issue_invoice": ["compute_cost", "process"],
    "close": ["document", "issue_invoice", "inform_customer"],
}


def total_orderings(prereqs):
    tasks = list(prereqs)
    result = []

    def visit(remaining, sequence):
        if not remaining:
            result.append(tuple(sequence))
            return
        for i in remaining:
            if all(o in sequence for o in prereqs[i]):
                sequence.append(i)
                visit([x for x in remaining if x != i], sequence)
                sequence.pop()
    visit(tasks, [])
    return result


def adjacency(prereqs):
    """Prerequisite mapping -> directed graph: an edge if a task depends on a prerequisite."""
    adj = {i: [] for i in prereqs}
    for i, predecessors in prereqs.items():
        for o in predecessors:
            adj[o].append(i)
    return adj


def topological_sort(adj):
    """The in-degree-based procedure from the Data Structures course, unchanged."""
    in_degree = {v: 0 for v in adj}
    for v in adj:
        for t in adj[v]:
            in_degree[t] += 1
    queue = deque(sorted(v for v in in_degree if in_degree[v] == 0))
    result = []
    while queue:
        v = queue.popleft()
        result.append(v)
        for t in adj[v]:
            in_degree[t] -= 1
            if in_degree[t] == 0:
                queue.append(t)
    return result if len(result) == len(adj) else None


def layers(adj):
    in_degree = {v: 0 for v in adj}
    for v in adj:
        for t in adj[v]:
            in_degree[t] += 1
    ready = sorted(v for v in in_degree if in_degree[v] == 0)
    result, processed = [], 0
    while ready:
        result.append(ready)
        processed += len(ready)
        next_ready = []
        for v in ready:
            for t in adj[v]:
                in_degree[t] -= 1
                if in_degree[t] == 0:
                    next_ready.append(t)
        ready = sorted(next_ready)
    return result if processed == len(adj) else None


def transitive_predecessors(prereqs):
    """Transitive closure: the tasks that must necessarily come before each task."""
    closure = {i: set(prereqs[i]) for i in prereqs}
    changed = True
    while changed:
        changed = False
        for i in closure:
            updated = set(closure[i])
            for o in closure[i]:
                updated |= closure[o]
            if updated != closure[i]:
                closure[i] = updated
                changed = True
    return closure


def incomparable_pairs(prereqs):
    closure = transitive_predecessors(prereqs)
    return [(a, b) for a, b in combinations(sorted(prereqs), 2)
            if b not in closure[a] and a not in closure[b]]


def factorial(n):
    s = 1
    for i in range(2, n + 1):
        s *= i
    return s


TASKS = len(ACTIVITY)
PREREQS = sum(len(v) for v in ACTIVITY.values())
ORDERINGS = total_orderings(ACTIVITY)
print("SYSTEM  : each run realizes a SINGLE execution order")
print("NOTATION:", TASKS + PREREQS, "symbols =", TASKS, "tasks +", PREREQS, "prerequisites")
print("COST    :", len(ORDERINGS), "total orderings are accepted | drawing a single ordering gives coverage",
      round(1 / len(ORDERINGS), 4))
print()
ADJ = adjacency(ACTIVITY)
print("topological sort gives a single output:")
print(" ", topological_sort(ADJ))
print("the same graph's number of valid outputs:", len(ORDERINGS))
LAYERS = layers(ADJ)
print("layers (tasks that can start at the same time):")
for i, layer in enumerate(LAYERS, 1):
    print(f"  {i}. {layer}")
print("critical path length:", len(LAYERS), "steps | sequential execution", TASKS, "steps")
print()
INCOMPARABLE = incomparable_pairs(ACTIVITY)
PAIRS = factorial(TASKS) // (factorial(2) * factorial(TASKS - 2))
print("task pairs:", PAIRS, "| pairs whose order the prerequisites fix:", PAIRS - len(INCOMPARABLE),
      "| free pairs:", len(INCOMPARABLE))
print("acceptance without prerequisites:", factorial(TASKS), "-> acceptance with prerequisites:", len(ORDERINGS),
      "| narrowing", factorial(TASKS) // len(ORDERINGS), "x")
print()
print("if one prerequisite is added, where does 168 land")
result = []
for a, b in INCOMPARABLE:
    for x, y in ((a, b), (b, a)):
        updated = {i: list(v) for i, v in ACTIVITY.items()}
        updated[y].append(x)
        if topological_sort(adjacency(updated)) is None:
            continue
        result.append((len(total_orderings(updated)), f"{x} -> {y}"))
result.sort()
print("  number of prerequisites that can be added:", len(result))
print("  the three choices that narrow the most:")
for n, name in result[:3]:
    print(f"    {name:38s} {len(ORDERINGS)} -> {n}")
print("  the three choices that narrow the least:")
for n, name in result[-3:]:
    print(f"    {name:38s} {len(ORDERINGS)} -> {n}")
print("  average of all of them:", round(sum(n for n, _ in result) / len(result), 1))
print("  the two choices that are meaningful in the workshop (DD9):")
for x, y in (("inform_customer", "process"), ("document", "issue_invoice")):
    updated = {i: list(v) for i, v in ACTIVITY.items()}
    updated[y].append(x)
    print(f"    {x + ' -> ' + y:38s} {len(ORDERINGS)} -> {len(total_orderings(updated))}")
print()
# DD10: decision node. The rework branch adds an optional task.
BRANCH = {i: list(v) for i, v in ACTIVITY.items()}
BRANCH["rework"] = ["process"]
BRANCH["document"] = ["rework"]
ORDERINGS2 = total_orderings(BRANCH)
print("if a decision node is added (the rework branch):")
print("  symbols", TASKS + PREREQS, "->",
      len(BRANCH) + sum(len(v) for v in BRANCH.values()) + 1, "(decision node included)")
print("  branch A", len(ORDERINGS), "total orderings | branch B", len(ORDERINGS2), "total orderings | total accepted",
      len(ORDERINGS) + len(ORDERINGS2))
```

```
SYSTEM  : each run realizes a SINGLE execution order
NOTATION: 23 symbols = 10 tasks + 13 prerequisites
COST    : 168 total orderings are accepted | drawing a single ordering gives coverage 0.006

topological sort gives a single output:
  ['intake', 'allocate_stock', 'assign_technician', 'inform_customer', 'compute_cost', 'reserve_workbench', 'process', 'document', 'issue_invoice', 'close']
the same graph's number of valid outputs: 168
layers (tasks that can start at the same time):
  1. ['intake']
  2. ['allocate_stock', 'assign_technician', 'inform_customer']
  3. ['compute_cost', 'reserve_workbench']
  4. ['process']
  5. ['document', 'issue_invoice']
  6. ['close']
critical path length: 6 steps | sequential execution 10 steps

task pairs: 45 | pairs whose order the prerequisites fix: 31 | free pairs: 14
acceptance without prerequisites: 3628800 -> acceptance with prerequisites: 168 | narrowing 21600 x

if one prerequisite is added, where does 168 land
  number of prerequisites that can be added: 28
  the three choices that narrow the most:
    compute_cost -> assign_technician      168 -> 16
    document -> compute_cost               168 -> 24
    issue_invoice -> inform_customer       168 -> 30
  the three choices that narrow the least:
    inform_customer -> issue_invoice       168 -> 138
    compute_cost -> document               168 -> 144
    assign_technician -> compute_cost      168 -> 152
  average of all of them: 84.0
  the two choices that are meaningful in the workshop (DD9):
    inform_customer -> process             168 -> 96
    document -> issue_invoice              168 -> 96

if a decision node is added (the rework branch):
  symbols 23 -> 26 (decision node included)
  branch A 168 total orderings | branch B 324 total orderings | total accepted 492
```

Three numbers: the system realizes **a single order** on every run, the notation writes
**23 symbols**, the cost is accepting **168 total orderings**. Drawing a single order
gives a coverage of 1/168, that is, 0.006.

#### One Output and One Hundred Sixty-Eight Outputs

The in-degree-based procedure returns one ordering, and that ordering is valid. But it
is not the only valid one; the procedure always gives the same output because it makes
its choice alphabetically whenever more than one task is in the queue. This is not a
property of the notation; it is **a criterion put in place to make the procedure
deterministic**. A different criterion would give a different order, and that order
would be valid too.

The answer to how many would be valid is 168. Of the forty-five task pairs, the
prerequisites fix the order of 31, and **14 are left free**. The free pairs are not
independent of one another — fixing one constrains the others — so the number is not
two to the fourteenth power but the 168 found by exhaustive count.

The real result here is how much work the notation is doing. If no prerequisite had been
written, the ten tasks could be done in **3,628,800** distinct orders, the product going
from ten down to one. Thirteen prerequisites bring this number down to 168: a
**narrowing of 21,600 times**. The notation carries serious information. And yet 168
remains, and 168 is not zero.

#### Concurrent Branches and Layers

The layer list is the same procedure's second output: the set of tasks that enter the
queue on each round. The shop's tasks split into six layers. In the second layer, three
tasks become ready at once — informing the customer, allocating stock, assigning a
technician — and none of the three waits on the others.

The number of layers is the **critical path length**: the shortest number of steps in
which all the work could finish with unlimited parallelism. Six steps stand against
sequential execution's ten steps. The concurrent-branch notation is what writes this
difference; a notation that does not draw the branches reads like a ten-step chain, and
the four-step gain becomes invisible.

The relationship between the layer structure and 168 is direct: the looseness arises
exactly from tasks within a layer being free relative to one another. Concurrency and
ambiguity are two names for the same fact here — **the notation can only state
parallelism by leaving order free.**

#### The Count of Adding One Prerequisite

Twenty-eight distinct prerequisites can be added without creating a cycle. Each sends
168 to a different place, and the range is wide: the narrowest result is **16**, the
widest is **152**, the average 84.

The difference does not come from the length of the written sentence. Each choice is a
single prerequisite and adds a single symbol to the notation; yet one symbol brings 168
down to 16 while another leaves it at 152 — saying almost nothing. **How much of the
ambiguity a symbol removes is determined by where that symbol is placed**, and this
share can vary by more than a factor of nine.

The choice that narrows the most is not the choice that means the most in the shop.
Forcing computing cost before assigning a technician brings 168 down to 16, but such a
rule has no counterpart in how the shop actually operates. The two rules that are
genuinely defensible in the shop — the customer is informed before processing begins,
the document is prepared before the invoice is issued (**DD9**) — bring 168 down to
**96**. The right number is not the smallest number; the right number is the one left
by the constraints that genuinely exist. Writing a constraint that does not exist in
order to narrow a notation replaces ambiguity with a lie.

A side effect of this is that the two kinds of loss separated in the use case lesson
show up here as well. Some of the 14 free pairs are **genuinely free** — the shop can do
those two tasks in whichever order it likes. Some are only **unwritten**: the order is
in fact fixed, but the prerequisite was never put on paper. The notation writes both the
same way, because a partial order has no separate symbol for "free" versus "unknown."
How many of the fourteen pairs are of which kind cannot be read from the diagram; it can
only be separated by asking the process owner one pair at a time. Until that separation
is made, the number 168 is the looseness's **upper bound**, not its measure.

#### A Decision Node Multiplies Ambiguity

The activity notation's third element is the decision: at some point the flow splits
into two branches based on a condition. Such a branch exists in the shop — if the
post-processing inspection fails, the work is reprocessed (**DD10**).

Once the decision node is added, the notation rises from 23 symbols to 26. The number of
accepted total orderings, however, is no longer 168 but the sum of the two branches:
branch A gives 168, branch B gives **324**, the total is **492**. Adding one node and
two edges nearly tripled the number of accepted executions.

The reason is this: the notation writes **under which condition** a branch is chosen as
text, but carries no structure that tests that text. To someone looking at the diagram,
both branches are always open. A decision looks like something that splits the flow;
measured, it turns out to be something that **merges**: it gathers two separate sets of
executions under a single notation.

#### Summary

- The activity notation does not write order, it writes prerequisites; ten tasks and
  thirteen prerequisites build a partial order with **23 symbols**, and that partial
  order accepts **168 total orderings** at once. Drawing a single order gives a coverage
  of 0.006.
- Topological sort gives a single output because it uses a criterion for choosing from
  the queue; the number of valid outputs is 168. Of the forty-five task pairs, 31 are
  fixed and 14 are free, and because the free pairs are not independent, the number is
  found by exhaustive count.
- The ten tasks without prerequisites could be done in 3,628,800 orders; thirteen
  prerequisites narrow this by **21,600 times**. Unless the looseness is written, a
  reader mistakes 168 for 1.
- The tasks split into six layers: critical path 6 steps, sequential execution 10
  steps. Parallelism and ambiguity are two faces of the same fact; the notation can
  state parallelism only by leaving order free.
- Of the 28 prerequisites that can be added without creating a cycle, one brings 168
  down to 16 while another leaves it at 152; the two rules that are genuinely
  defensible in the shop give 96. The constraint that narrows the most is not the most
  correct constraint.
- A decision node raises 23 symbols to 26 while carrying the accepted total orderings
  from 168 to **492**: a decision does not split the flow, it merges two sets of
  executions into a single notation.

#### Next Step

This lesson counted the executions the notation **over-accepts**: all 168 orders are
valid, but the system does only one of them on any given run. The acceptance was
excessive, and all of the excess was deliberate — a partial order is written precisely
to be loose.

The next notation produces the same excess without meaning to, and on top of that does
the opposite as well. A state machine writes the states an object can pass through and
the transitions between them; in the shop, a work order has four states and six
transitions. This machine **rejects two** of the six sequences that genuinely occur and
**accepts two** sequences that do not occur in the system. When two transitions are
added to rescue the rejected ones, the rejected count drops to zero, but
over-approximation rises from 2 to **26**. The lesson writes this as a rule: the price
of closing under-approximation is over-approximation.

### State Machine Diagrams

Source: https://academia.sh/en/courses/modeling-and-representation/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.

## Data Modeling

What the entity-relationship model states, the constraints dropped in the shift between three abstraction levels, and the decisions a physical schema invents that the model never states.

### The Entity–Relationship Model

Source: https://academia.sh/en/courses/modeling-and-representation/entity-relationship-model

The conceptual model of a maintenance shop states 26 facts: 5 entities, 12 attributes, 4 relationships, 5 constraints. When cardinality is written but participation is not, 1,048,576 systems collapse onto the same notation.

The previous lesson represented a work order's behavior over time with four states and
six transitions. The measure was the gap between the language the machine accepts and
the system's real traces: two sequences were rejected, two sequences absent from the
system were accepted, and when two transitions were added, rejection dropped to zero
while over-acceptance rose from 2 to 26. That representation described **what the
system does**.

This topic takes up the representation that describes what the system **holds**. Its
question has the same shape: writing the domain's data structure in terms of entities,
attributes, and relationships states how many facts, and leaves how many unstated. The
relational model, key types, integrity constraints, and normalization are not taught
here; they were established in the Data Modeling and Relational Theory course of the
Databases curriculum and are not repeated. The only task here is **counting the facts
that a change of level drops and invents.**

#### Entity, Attribute, Relationship

An **entity** is a concept in the domain that has an identity of its own: a work
order, a technician, a part. An **attribute** is a single-valued fact carried by an
entity: a work order's opening date, a technician's specialty. A **relationship** is a
link between two entities and carries a **cardinality** — a label stating which side
is one and which side is many.

The term entity here names a carrier of identity; the **entity object** used in the
Databases curriculum, by contrast, is a programming object in the domain layer and is
a separate concept.

- **VM1** — The modeled domain is a maintenance shop, and the system is data. No
  modeling tool, drawing product, or data storage product is invoked; what is measured
  is the data itself.
- **VM2** — The conceptual model's **fact count** is the sum of the counts of
  entities, attributes, relationships, and constraints. Every fact is counted once.

```python
"""M01/K06 common definition (excerpt): the conceptual entity-relationship model and fact counting."""
CONSTRAINTS = [
    {"text": "every work order has a single technician",      "tool": "foreign key"},
    {"text": "part code is unique",                            "tool": "uniqueness"},
    {"text": "a document belongs only to its own work order",  "tool": "foreign key"},
    {"text": "a work order requires at least one part",        "tool": None},
    {"text": "a technician is at only one machine at a time",  "tool": None},
]
CONCEPTUAL = {
    "entities": ["WorkOrder", "Technician", "Part", "Machine", "Document"],
    "attributes": {"WorkOrder": ["number", "opened", "status"], "Technician": ["name", "specialty"],
                   "Part": ["code", "name", "unit"], "Machine": ["code", "type"],
                   "Document": ["type", "date"]},
    "relationships": [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                       ("Technician", "Machine", "N:M"), ("WorkOrder", "Document", "1:N")],
    "constraints": CONSTRAINTS,
}


def facts(model):
    e = len(model["entities"])
    attr = sum(len(v) for v in model["attributes"].values())
    r = len(model["relationships"])
    c = len(model["constraints"])
    return {"entity": e, "attribute": attr, "relationship": r, "constraint": c, "total": e + attr + r + c}


print("conceptual model:", facts(CONCEPTUAL))
print()
print("entity      attributes  relationships involved")
for name in CONCEPTUAL["entities"]:
    links = [f"{a}-{b} {c}" for a, b, c in CONCEPTUAL["relationships"] if name in (a, b)]
    print(f"{name:10s}  {len(CONCEPTUAL['attributes'][name]):9d}  {', '.join(links)}")
```

```
conceptual model: {'entity': 5, 'attribute': 12, 'relationship': 4, 'constraint': 5, 'total': 26}

entity      attributes  relationships involved
WorkOrder           3  WorkOrder-Technician N:1, WorkOrder-Part N:M, WorkOrder-Document 1:N
Technician          2  WorkOrder-Technician N:1, Technician-Machine N:M
Part                3  WorkOrder-Part N:M
Machine             2  Technician-Machine N:M
Document            2  WorkOrder-Document 1:N
```

**System:** the maintenance shop's conceptual model states **26 facts**. **Notation:**
the same 26 items are written into the diagram — five entity boxes, twelve
attributes, four relationship lines, and five constraint notes. It looks like a
one-to-one match. The **cost** is what falls outside this match, and it will be
counted in the next section.

#### What Cardinality States, What Multiplicity Adds

In this course, two separate terms carry two separate pieces of information, and
confusing them corrupts the measurement. **Cardinality** is the label of the
entity–relationship notation and is written on the relationship **as a whole**:
`N:1`, `1:N`, `N:M`. What it states is the **upper bound** at each end — which side
can have at most one, which side can have an unbounded number. **Multiplicity**, on
the other hand, is the label of the structural notation and is written **separately
on each end** of the relationship; its form is a `lower..upper` range.

The difference is exactly this: cardinality states the upper bounds, multiplicity
adds the **lower bound** alongside the upper bound. The lower bound is whether that
end's **participation** is mandatory or optional. The `N:1` label states that a work
order has at most one technician; it does not state **whether a work order can be
opened without a technician**.

- **VM3** — Every relationship has two ends, and each end participates mandatorily or
  optionally, independently. Cardinality carries only the upper bounds.
- **VM4** — Every attribute is independently mandatory or optional. The
  participation space and the requiredness space are independent of each other; the
  total count is their product.
- **VM5** — The count is exhaustive, not a sample; even a difference of one system is
  meaningful.

```python
"""What cardinality writes, what multiplicity adds: how many systems fall onto the same notation."""
from itertools import product

RELATIONSHIPS = [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                  ("Technician", "Machine", "N:M"), ("WorkOrder", "Document", "1:N")]
UPPER = {"N:1": ("*", "1"), "1:N": ("1", "*"), "N:M": ("*", "*"), "1:1": ("1", "1")}
ATTRIBUTES = 12          # the conceptual model's 12 attributes


def er_notation(system, multiplicity=False):
    """system: a list of (source, target, cardinality, lower_source, lower_target)."""
    written = []
    for source, target, card, lower_s, lower_t in system:
        entry = {"source": source, "target": target, "cardinality": card}
        if multiplicity:
            upper_s, upper_t = UPPER[card]
            entry["multiplicity_source"] = f"{lower_s}..{upper_s}"
            entry["multiplicity_target"] = f"{lower_t}..{upper_t}"
        written.append(entry)
    return written


def same_notation_count(system, multiplicity=False):
    """Exhaustive count: how many separate participation choices produce the same notation."""
    target_notation = er_notation(system, multiplicity)
    count = 0
    for choice in product((0, 1), repeat=2 * len(system)):
        candidate = [(s, t, c, choice[2 * i], choice[2 * i + 1])
                     for i, (s, t, c, _, _) in enumerate(system)]
        if er_notation(candidate, multiplicity) == target_notation:
            count += 1
    return count


SYSTEM = [(s, t, c, 1, 1) for s, t, c in RELATIONSHIPS]
print("relationship ends:", 2 * len(SYSTEM), "| unwritten participation bits:", 2 * len(SYSTEM))
print("cardinality written, multiplicity not written ->",
      same_notation_count(SYSTEM, multiplicity=False), "systems")
print("multiplicity written                          ->",
      same_notation_count(SYSTEM, multiplicity=True), "systems")
print()
attribute_space = sum(1 for _ in product((0, 1), repeat=ATTRIBUTES))
print("attribute requiredness not written ->", attribute_space, "systems")
print("the two spaces are independent, product ->",
      same_notation_count(SYSTEM) * attribute_space, "systems")
print()
print("written items  what was added                    falls onto same notation")
print(f"           26  entity+attribute+relationship+constraint  "
      f"{same_notation_count(SYSTEM) * attribute_space:20d}")
print(f"           34  + 8 participation marks                   "
      f"{same_notation_count(SYSTEM, True) * attribute_space:20d}")
print(f"           46  + 12 requiredness marks                   {1:20d}")
```

```
relationship ends: 8 | unwritten participation bits: 8
cardinality written, multiplicity not written -> 256 systems
multiplicity written                          -> 1 systems

attribute requiredness not written -> 4096 systems
the two spaces are independent, product -> 1048576 systems

written items  what was added                    falls onto same notation
           26  entity+attribute+relationship+constraint               1048576
           34  + 8 participation marks                                   4096
           46  + 12 requiredness marks                                      1
```

**This is the cost.** A conceptual model written with twenty-six facts **cannot be
distinguished from 1,048,576 other systems** that resemble it. This number comes from
two independent sources: the participation information at the eight ends of the four
relationships (256), and the requiredness information of the twelve attributes
(4096). Neither is written in the notation, but both are determinate in the system.

The "every symbol is a bit" relationship measured in the Structural Diagrams topic
holds here too; it is not repeated. What this lesson adds is **which bits** are
dropped, and this choice is not random: the entity–relationship notation was
historically designed to carry cardinality, not to carry participation. The twenty
dropped bits do not vanish — as will soon be seen, at the logical and physical levels
they turn into **a decision someone else will make**. Whether an attribute is
mandatory will reappear at the physical level under the name "nullability," and
giving an answer at that level will be **mandatory**.

#### Entity or Attribute

The most frequently made decision in the model is whether a fact will be its own
entity or an attribute of another entity. This decision is not a matter of style; **it
changes, together, the number of facts the model can state and the number of states it
can represent.**

- **VM6** — The pool of documents that can be attached to a work order has four
  candidates. The entity form can represent any subset of this pool; the attribute
  form carries a single value, that is, at most one item.
- **VM7** — When the `unit` attribute is promoted to its own entity, the value set
  closes, and the constraint "unit only from the defined set" becomes statable.

```python
"""Same domain, two separate entity-attribute decisions: how many facts change, how many states are lost."""
from itertools import combinations

CONCEPTUAL = {
    "entities": ["WorkOrder", "Technician", "Part", "Machine", "Document"],
    "attributes": {"WorkOrder": ["number", "opened", "status"], "Technician": ["name", "specialty"],
                   "Part": ["code", "name", "unit"], "Machine": ["code", "type"],
                   "Document": ["type", "date"]},
    "relationships": [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                       ("Technician", "Machine", "N:M"), ("WorkOrder", "Document", "1:N")],
    "constraints": 5,
}
DEMOTED = {                          # Document steps down from entity to attribute
    "entities": ["WorkOrder", "Technician", "Part", "Machine"],
    "attributes": {"WorkOrder": ["number", "opened", "status", "document"], "Technician": ["name", "specialty"],
                   "Part": ["code", "name", "unit"], "Machine": ["code", "type"]},
    "relationships": [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                       ("Technician", "Machine", "N:M")],
    "constraints": 4,                # "a document belongs only to its own work order" cannot be stated
}
PROMOTED = {                         # unit steps up from attribute to entity
    "entities": ["WorkOrder", "Technician", "Part", "Machine", "Document", "Unit"],
    "attributes": {"WorkOrder": ["number", "opened", "status"], "Technician": ["name", "specialty"],
                   "Part": ["code", "name"], "Machine": ["code", "type"],
                   "Document": ["type", "date"], "Unit": ["name"]},
    "relationships": [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                       ("Technician", "Machine", "N:M"), ("WorkOrder", "Document", "1:N"),
                       ("Part", "Unit", "N:1")],
    "constraints": 6,                # "unit only from the defined set" can now be stated
}


def facts(model):
    return (len(model["entities"]) + sum(len(a) for a in model["attributes"].values())
            + len(model["relationships"]) + model["constraints"])


print("decision            entity  attribute  relationship  constraint  facts")
for name, m in (("common definition", CONCEPTUAL), ("Document to attribute", DEMOTED),
                ("unit to entity", PROMOTED)):
    print(f"{name:22s}  {len(m['entities']):6d}  "
          f"{sum(len(a) for a in m['attributes'].values()):9d}  "
          f"{len(m['relationships']):12d}  {m['constraints']:10d}  {facts(m):5d}")
print()
CANDIDATE = ("acceptance form", "fault report", "test record", "delivery report")
all_states = [k for n in range(len(CANDIDATE) + 1) for k in combinations(CANDIDATE, n)]
single_valued = [d for d in all_states if len(d) <= 1]
print("a work order's document state -> total", len(all_states),
      "| entity form shows:", len(all_states),
      "| attribute form:", len(single_valued))
print("states that become unrepresentable:", len(all_states) - len(single_valued))
```

```
decision            entity  attribute  relationship  constraint  facts
common definition            5         12             4           5     26
Document to attribute        4         11             3           4     22
unit to entity               6         12             5           6     29

a work order's document state -> total 16 | entity form shows: 16 | attribute form: 5
states that become unrepresentable: 11
```

Demoting the document to an attribute drops the model from **26 facts to 22 facts**:
one entity, one attribute, one relationship, and one constraint disappear. The
constraint that disappears is "a document belongs only to its own work order," and
the reason is structural — since the document is no longer a separate thing, the
question of whose it is can no longer be asked. The heavier consequence is in the
second row: of the **16 possible states** for a work order's document status, **11**
can no longer be represented, because a single-valued attribute can only stay empty
or carry a single document.

The opposite direction is not free either. Promoting the `unit` attribute to its own
entity raises the fact count from **26 to 29**: one entity, one relationship, and one
constraint are added. The payoff is concrete — the set of unit values closes, meaning
a unit not defined in the model cannot be written. The cost of promotion is three
facts' worth of extra maintenance load; the cost of demotion is four facts' worth of
loss and eleven unrepresentable states. **Neither is free, and both are countable.**

A decision criterion follows from this, and the criterion is not a matter of taste
but the answer to three questions. Whether a fact deserves to be its own entity is
judged by: does it have its own attributes, does it need to be referred to on its
own, and can it connect to a parent entity **more than once**. Document answers yes
to all three — it has a type and a date, the work order it belongs to can be asked,
and a work order carries more than one document. `unit`, however, answers no on the
third: a part has a single unit. This is why promoting unit does not change the
number of representable states at all; it only gains a constraint and adds three
facts. **The decision is a comparison between the constraint gained and the fact
added.**

#### Constraints Are Facts Too

Five of the twenty-six facts are constraints, and they live not in the diagram's
lines but in notes written beside it. This distinction is the notation's weakest
point: the box and the arrow are part of a formal visual language and are processed
mechanically, while the note is free text. Three of the five constraints can be
written with the relational structure's tools, two cannot — which is which, and the
**structural reason** for the inability, is the subject of the next lesson.

Looking at the shape of two constraints already hints at where the difference comes
from. "Part code is unique" looks at a single column and compares two rows. "A work
order requires at least one part," however, cannot be answered by looking at a single
row; it asks about the **number of rows** attached to a work order. The third is
further still: "a technician is at only one machine at a time" asks whether two
rows' time intervals **intersect**, and the conceptual model has no attribute called
time. The three constraints are three separate kinds of predicate; that one can be
written does not mean the other can be too.

What must be noted for now is this: a constraint, as long as it is a note in the
notation, **is a fact of the model**; but if it cannot be converted into a
transportable structure on the way down from the notation to the schema, it
disappears without anyone deleting it.

#### Summary

- The conceptual model states 26 facts for the maintenance shop: 5 entities, 12
  attributes, 4 relationships, 5 constraints. Fact counting is this topic's unit of
  measure.
- Cardinality is written on the relationship as a whole and states only the upper
  bounds; multiplicity is written separately on each end and also carries the lower
  bound, that is, participation. The difference is two bits per relationship.
- When cardinality is written but participation and attribute requiredness are not,
  1,048,576 systems fall onto the same notation: the product of 256 participation
  choices and 4096 requiredness choices. Adding eight participation marks leaves
  4096; adding all twelve requiredness marks as well leaves 1.
- Modeling a fact as an attribute instead of an entity reduces the model from 26
  facts to 22 and makes 11 of a work order's 16 document states unrepresentable;
  promoting in the opposite direction raises the fact count to 29 and makes a
  constraint statable.
- Constraints are facts of the model too, but because they stand as notes in the
  diagram, whether they carry over depends not on the formal visual language but on
  the tools of the next level.

#### Next Step

This lesson kept the model at a single level and counted what that level does not
state. The next lesson changes the level: as the same 26 facts are converted into a
relational structure, two N:M relationships open into a junction table, the table
count rises from 5 to 7, and two of the five constraints drop. Why the two dropped
constraints drop is not guessed at; which tool cannot write which predicate is shown
by exhaustive count.

### Conceptual, Logical, and Physical Model

Source: https://academia.sh/en/courses/modeling-and-representation/conceptual-logical-and-physical-model

When the same 26 facts are converted into a relational structure, the table count rises from 5 to 7, 13 fields appear that are never named in the model, and 2 of the 5 constraints drop; the structural reason for the two that drop is shown by exhaustive count.

The previous lesson kept the conceptual model at a single level and counted what that
level did not state: twenty-six facts were written, but because participation and
attribute requiredness were not written, more than a million systems fell onto the
same notation. This lesson changes the level.

Its question is this: when the same twenty-six facts are converted into a relational
structure, **how much carries over as is, how much drops, and how much appears that
never was written in the model.** Three numbers will again stand side by side, but
this time the cost has two separate components: the **dropped constraint** and the
**invented field.** They are counted separately, because one is a loss and the other
an addition.

#### Three Levels

The **conceptual model** carries the domain's facts and contains no storage decision:
entity, attribute, relationship, constraint. The **logical model** converts these
facts into the structures of a particular data model; here that structure is
relational, meaning table, field, key, and foreign key. The **physical model** makes
the same structure storable: type, length, nullability, index. None of the three
levels contains a product name; even the physical level carries **kinds of
decision**, not a product.

- **VM8** — The three levels sit on top of one another, and the number of decisions
  grows as you go down. Everything a level does not state is either derived or
  invented at the level below.
- **VM9** — The conceptual-to-logical rule is fixed: every entity converts to a
  table, every N:M relationship opens into a **junction table**, every N:1 and 1:N
  relationship becomes a foreign key field placed on the many side, and every table
  gets a primary key field added.
- **VM10** — A constraint **carries over** if it can be written with one of the
  relational structure's tools — key, uniqueness, or foreign key; it **drops** if it
  cannot.

Key types, foreign-key delete and update actions, integrity constraints, and
normalization are not taught in this lesson; all of them were established in the
Databases curriculum's Data Modeling and Relational Theory course. Here those tools
are used only as **carriers**, and the only question asked is which fact carries
over and which does not.

```python
"""M01/K06 common definition (excerpt): the conceptual-to-logical transition and its balance sheet."""
CONSTRAINTS = [
    {"text": "every work order has a single technician",      "tool": "foreign key"},
    {"text": "part code is unique",                            "tool": "uniqueness"},
    {"text": "a document belongs only to its own work order",  "tool": "foreign key"},
    {"text": "a work order requires at least one part",        "tool": None},
    {"text": "a technician is at only one machine at a time",  "tool": None},
]
CONCEPTUAL = {
    "entities": ["WorkOrder", "Technician", "Part", "Machine", "Document"],
    "attributes": {"WorkOrder": ["number", "opened", "status"], "Technician": ["name", "specialty"],
                   "Part": ["code", "name", "unit"], "Machine": ["code", "type"],
                   "Document": ["type", "date"]},
    "relationships": [("WorkOrder", "Technician", "N:1"), ("WorkOrder", "Part", "N:M"),
                       ("Technician", "Machine", "N:M"), ("WorkOrder", "Document", "1:N")],
    "constraints": CONSTRAINTS,
}


def logical(conceptual):
    """N:M relationships open into a junction table; constraints are split by transportability."""
    tables = {}
    for e in conceptual["entities"]:
        tables[e] = ["key"] + list(conceptual["attributes"][e])
    junction = []
    for s, t, card in conceptual["relationships"]:
        if card == "N:M":
            name = f"{s}_{t}"
            tables[name] = ["key", f"{s}_key", f"{t}_key"]
            junction.append(name)
        elif card == "N:1":
            tables[s].append(f"{t}_key")
        else:
            tables[t].append(f"{s}_key")
    carried = [k for k in conceptual["constraints"] if k["tool"]]
    dropped = [k for k in conceptual["constraints"] if not k["tool"]]
    return {"tables": tables, "junction_tables": junction, "carried": carried, "dropped": dropped}


L = logical(CONCEPTUAL)
print("table           fields  field names")
for name, fields in L["tables"].items():
    print(f"{name:20s}  {len(fields):4d}  {', '.join(fields)}")
print()
field = sum(len(v) for v in L["tables"].values())
attribute = sum(len(a) for a in CONCEPTUAL["attributes"].values())
key = sum(v.count("key") for v in L["tables"].values())
foreign = sum(1 for v in L["tables"].values() for a in v if a.endswith("_key"))
print("entity", len(CONCEPTUAL["entities"]), "-> table", len(L["tables"]),
      "( junction table:", ", ".join(L["junction_tables"]), ")")
print("attribute", attribute, "-> field", field,
      "| field never named in the conceptual model:", field - attribute)
print("  ", key, "primary key +", foreign, "foreign key =", key + foreign)
print("constraint", len(CONSTRAINTS), "-> carried", len(L["carried"]), "| dropped", len(L["dropped"]))
for k in L["dropped"]:
    print("   DROPPED:", k["text"])
```

```
table           fields  field names
WorkOrder                5  key, number, opened, status, Technician_key
Technician               3  key, name, specialty
Part                     4  key, code, name, unit
Machine                  3  key, code, type
Document                 4  key, type, date, WorkOrder_key
WorkOrder_Part           3  key, WorkOrder_key, Part_key
Technician_Machine       3  key, Technician_key, Machine_key

entity 5 -> table 7 ( junction table: WorkOrder_Part, Technician_Machine )
attribute 12 -> field 25 | field never named in the conceptual model: 13
   7 primary key + 6 foreign key = 13
constraint 5 -> carried 3 | dropped 2
   DROPPED: a work order requires at least one part
   DROPPED: a technician is at only one machine at a time
```

**System:** 26 facts. **Notation:** 7 tables and 25 fields. **Cost:** 2 dropped
constraints and **13 invented fields.** The third number gets the least attention but
is the most revealing: the logical model carries **thirteen fields** whose name the
conceptual model never mentions. Seven are the tables' primary keys, six are the
foreign keys that establish the relationships. None of these is a fact of the
domain — nobody in the shop talks about a work order's "key." These are **the
relational structure's own record-keeping apparatus**, and they come from the level,
not from the model.

The table count rising from 5 to 7 comes from the same place. A relational structure
cannot hold an N:M relationship directly; it requires a **junction table** carrying
the keys of both sides. Because there are two N:M relationships, two junction tables
are born, and the field count rises by six. This is not a design preference; it is
the direct consequence of the relational structure's **expressive limit**.

#### Why the Two Dropped Constraints Drop

Three of the five constraints carry over: "every work order has a single technician"
can be written with a foreign key, "part code is unique" with a uniqueness
constraint, "a document belongs only to its own work order" again with a foreign
key. The remaining two drop, and the reason for dropping is not forgetfulness but
**the type of the predicate.**

The key, uniqueness, and foreign-key tools all have the same shape: they either look
at a **single row**, or they compare the **equality** of specific fields across two
rows. The two dropped constraints fall outside these two shapes. "A work order
requires at least one part" cannot be answered by looking at a single row; it looks
at the **number** of rows attached to a work order and needs a **lower bound** on
that number. "A technician is at only one machine at a time" asks whether two rows'
time intervals **intersect**, whereas uniqueness sees only **equality**.

- **VM11** — The over-acceptance count uses two work orders, two parts, two
  technicians, and two machines. The sample space is every subset of rows the
  junction table could take; the count is exhaustive.
- **VM12** — The instances show a state at a single instant; the table has no time
  field.

```python
"""Why the two dropped constraints drop: which predicate the relational tool cannot write."""
from itertools import combinations, product

WORK_ORDER, PART = ("o1", "o2"), ("p1", "p2")
TECHNICIAN, MACHINE = ("u1", "u2"), ("t1", "t2")


def all_instances(left, right):
    """All values the junction table could take: every subset of the row set."""
    rows = list(product(left, right))
    return [frozenset(k) for n in range(len(rows) + 1) for k in combinations(rows, n)]


def tool_accepts(instance, left, right):
    """Cuts out composite-key row repetition and foreign-key dangling references.
    Both are predicates that look at a SINGLE ROW."""
    return all(a in left and b in right for a, b in instance)


def at_least_one_part(instance, left):
    """A predicate that looks at group SIZE: every work order must have at least one row."""
    return all(any(source == x for source, _ in instance) for x in left)


def single_machine(instance):
    """Group size again: a technician must appear in at most one row."""
    used = [a for a, _ in instance]
    return len(used) == len(set(used))


total_accepted, total_correct = 1, 1
for name, left, right, correct in (("WorkOrder_Part", WORK_ORDER, PART,
                                    lambda o: at_least_one_part(o, WORK_ORDER)),
                                   ("Technician_Machine", TECHNICIAN, MACHINE, single_machine)):
    instances = all_instances(left, right)
    accepted = [o for o in instances if tool_accepts(o, left, right)]
    correct_ones = [o for o in accepted if correct(o)]
    total_accepted *= len(accepted)
    total_correct *= len(correct_ones)
    print(f"{name:19s} possible instances {len(instances):3d} | tool accepts {len(accepted):3d}"
          f" | correct in the system {len(correct_ones):3d} | over-acceptance {len(accepted) - len(correct_ones):3d}")
    print(f"{'':19s} smallest over-acceptance: "
          f"{sorted(sorted(o) for o in accepted if not correct(o))[0]}")
print()
print("both junction tables together: tool accepts", total_accepted, "| correct in the system", total_correct,
      "| over-acceptance", total_accepted - total_correct)
print()


def overlaps(x, y):
    return x[1] < y[2] and y[1] < x[2]


ROWS = [("u1", 0, 4), ("u1", 2, 6)]
print("even with a time field added:")
print("  the two rows have different values -> uniqueness passes:", len(set(ROWS)) == len(ROWS))
print("  the intervals overlap -> constraint violated:", overlaps(ROWS[0], ROWS[1]))
```

```
WorkOrder_Part      possible instances  16 | tool accepts  16 | correct in the system   9 | over-acceptance   7
                    smallest over-acceptance: []
Technician_Machine  possible instances  16 | tool accepts  16 | correct in the system   9 | over-acceptance   7
                    smallest over-acceptance: [('u1', 't1'), ('u1', 't2')]

both junction tables together: tool accepts 256 | correct in the system 81 | over-acceptance 175

even with a time field added:
  the two rows have different values -> uniqueness passes: True
  the intervals overlap -> constraint violated: True
```

For both junction tables, the tools accept **16 of 16 instances**, whereas only **9**
are correct in the system. Seven instances each are states the schema accepts but
that should never exist in the domain — in the course's term, **over-acceptance**.
Taken together, the two tables give a schema that accepts 256 states, of which 81
are correct in the system, a difference of **175**.

The smallest counterexamples show the type of each constraint in its bare form. The
smallest violation of the "at least one part" constraint is an **empty table**: with
no rows at all, no key and no foreign key is violated, yet every work order is left
without a part. Writing a lower-bound constraint with a tool that operates at the
row level is structurally impossible, because the violation lies **not in a row that
exists, but in a row that does not.** The smallest violation of the second
constraint is two rows: `('u1','t1')` and `('u1','t2')`. The composite key counts
these two rows as separate, because they genuinely are different; the question being
asked is not equality, but **how many times the same technician appears.**

The last two rows show why the attempt to rescue the constraint by adding a time
field is not enough. `('u1', 0, 4)` and `('u1', 2, 6)` are different values, so they
pass every uniqueness constraint; but their intervals overlap and the constraint is
violated. **Equality and intersection are separate predicates,** and one cannot
express the other.

#### Second Configuration

A measurement is understood only when it is tested by varying something. Two changes
are tried: varying the cardinalities and adding a new field to the model.

- **VM13** — The time field has two slots (0 and 1); the `time` field is a field not
  present in the conceptual model, added at the logical level.

```python
"""Second configuration: two ways to carry the dropped constraint, and the cost of each."""
from itertools import combinations, product

TECHNICIAN, MACHINE, TIME = ("u1", "u2"), ("t1", "t2"), (0, 1)


def subsets(rows):
    return [frozenset(k) for n in range(len(rows) + 1) for k in combinations(rows, n)]


def schema(relationships, entity_count=5, attribute_count=12):
    table = entity_count + sum(1 for _, c in relationships if c == "N:M")
    field = table + attribute_count + sum(2 if c == "N:M" else 1 for _, c in relationships)
    return table, field


print("WorkOrder-Part  Technician-Machine  table  field  invented field")
for c1, c2 in product(("N:M", "N:1"), repeat=2):
    t, f = schema([("WorkOrder-Technician", "N:1"), ("WorkOrder-Part", c1),
                   ("Technician-Machine", c2), ("WorkOrder-Document", "1:N")])
    print(f"{c1:^12s}  {c2:^11s}  {t:5d}  {f:4d}  {f - 12:14d}")
print()
rows_timeless = list(product(TECHNICIAN, MACHINE))
rows_timed = list(product(TECHNICIAN, MACHINE, TIME))


def correct_timeless(instance):
    keys = [u for u, _ in instance]
    return len(keys) == len(set(keys))


def correct_timed(instance):
    keys = [(u, z) for u, _, z in instance]
    return len(keys) == len(set(keys))


for name, rows, correct, unique in (
        ("no time field  ", rows_timeless, correct_timeless, None),
        ("time field     ", rows_timed, correct_timed, None),
        ("time + unique  ", rows_timed, correct_timed, correct_timed)):
    instances = subsets(rows)
    accepted = [o for o in instances if unique is None or unique(o)]
    correct_ones = [o for o in accepted if correct(o)]
    print(f"{name}: tool accepts {len(accepted):4d} | correct in the system {len(correct_ones):3d}"
          f" | over-acceptance {len(accepted) - len(correct_ones):4d}")
```

```
WorkOrder-Part  Technician-Machine  table  field  invented field
    N:M           N:M          7    25              13
    N:M           N:1          6    23              11
    N:1           N:M          6    23              11
    N:1           N:1          5    21               9

no time field  : tool accepts   16 | correct in the system   9 | over-acceptance    7
time field     : tool accepts  256 | correct in the system  81 | over-acceptance  175
time + unique  : tool accepts   81 | correct in the system  81 | over-acceptance    0
```

The first table shows that the junction table is not a matter of preference: every
N:M relationship adds exactly one table and two fields, and in the fourth row, once
no N:M relationship remains, the table count drops to the entity count, that is, 5.
The invented field count also drops from 13 to 9 — because a key per table and at
least one foreign key per relationship are required in every case.

The second table is more interesting. Once the time field is added, the number of
**correct** instances the table can carry rises from 9 to **81**: the seventy-two
states in which the same technician works at different machines in different time
slots could not be written at all in the timeless form. Moreover, a uniqueness
constraint placed on `(technician, time)` carries the dropped constraint **exactly**:
over-acceptance drops from 175 to **0**.

This result shows a way to bring back the dropped constraints, but it also shows
their cost. The constraint could only be written after adding a field **not present
at all** in the conceptual model. That is, the way to rescue the dropped constraint
runs through putting something the model does not state into the schema — and no
fact in the conceptual model states why that field is there. This is the first
example of the next lesson's subject: **the logical and physical levels do not only
lose, they also invent.**

#### Summary

- The conceptual level carries the domain's facts, the logical level their
  relational-structure counterpart, the physical level storage decisions; the number
  of decisions grows going down.
- 26 facts convert at the logical level into 7 tables and 25 fields. Two N:M
  relationships give rise to two junction tables and raise the table count from 5 to
  7.
- The name of 13 of the 25 fields never appears in the conceptual model: 7 primary
  keys and 6 foreign keys. These are not facts of the domain but the relational
  structure's record-keeping apparatus.
- 3 of the 5 constraints carry over, 2 drop. The reason for dropping is the type of
  predicate: one puts a lower bound on row count, the other asks about the
  intersection of time intervals; key and uniqueness, in contrast, look at a single
  row or compare equality.
- The cost of the dropped constraints was counted: across the two junction tables
  the schema accepts 256 instances while 81 are correct in the system, that is, 175
  over-acceptances. The smallest violations are an empty table and two rows of the
  same technician.
- The constraint can be carried over exactly by adding a `time` field not present in
  the conceptual model (over-acceptance from 175 to 0), and the number of correct
  instances that can be carried rises from 9 to 81; the cost is putting a field the
  model does not state into the schema.

#### Next Step

This lesson showed that a field can appear without ever being named in the model, and
counted thirteen of them. The next lesson carries this count to the physical level,
where the situation changes radically: seven tables with twenty-five fields demand a
hundred field decisions and fourteen table decisions. How many times the total of one
hundred fourteen decisions is the twenty-six facts the conceptual model states, who
makes these decisions, and how it can be told that a decision made is wrong.

### From Model to Schema

Source: https://academia.sh/en/courses/modeling-and-representation/from-model-to-schema

For seven tables and twenty-five fields, 100 field decisions and 14 table decisions are given: a total of 114, 4.4 times the 26 facts the conceptual model states. Every decision the model does not state is a decision someone makes that does not appear in the model.

The previous lesson counted thirteen fields appearing at the logical level without
their name ever occurring in the model, and showed that rescuing a dropped constraint
runs, again, through adding a field not present in the model. This lesson descends to
the final level.

The physical schema makes the logical model storable. Its question is: how many
decisions does this conversion require, how many of these decisions does the
conceptual model determine, and who determines the ones it does not. The answer
produces the course's largest ratio, and it makes the course's rule pay out one last
time: **a notation's measure is not the symbols it carries but what it leaves to
someone else because it does not state it.**

#### The Questions the Physical Level Asks

Making a field storable requires answering four questions: which **type**, what
**length**, is it **nullable**, what is its **default value**. Making a table
storable adds two more questions: an **index** for the primary key and a **character
set**. None of these six questions is optional; if no answer is given, a default
takes its place, and a default is an answer too.

- **VM14** — At the physical level, every field asks four decisions, every table two.
  The list is the common definition and is not expanded in this lesson.
- **VM15** — A decision being **invented** means no fact of the conceptual model
  determines that decision. An invented decision is not a wrong decision; it is a
  decision **whose source is not in the model**.

```python
"""M01/K06 common definition (excerpt): the number of decisions invented by the physical level."""
FIELD_DECISION = ["type", "length", "nullability", "default value"]
TABLE_DECISION = ["primary key index", "character set"]

LOGICAL = {
    "WorkOrder": ["key", "number", "opened", "status", "Technician_key"],
    "Technician": ["key", "name", "specialty"],
    "Part": ["key", "code", "name", "unit"],
    "Machine": ["key", "code", "type"],
    "Document": ["key", "type", "date", "WorkOrder_key"],
    "WorkOrder_Part": ["key", "WorkOrder_key", "Part_key"],
    "Technician_Machine": ["key", "Technician_key", "Machine_key"],
}
CONCEPTUAL_FACTS = 26      # 5 entities + 12 attributes + 4 relationships + 5 constraints


def physical(tables):
    field = sum(len(v) for v in tables.values())
    table = len(tables)
    return {"table": table, "field": field,
            "field_decision": field * len(FIELD_DECISION),
            "table_decision": table * len(TABLE_DECISION),
            "invented_decision": field * len(FIELD_DECISION) + table * len(TABLE_DECISION)}


P = physical(LOGICAL)
print("table          fields  field decisions  table decisions  total")
for name, fields in LOGICAL.items():
    fd = len(fields) * len(FIELD_DECISION)
    td = len(TABLE_DECISION)
    print(f"{name:19s} {len(fields):4d}  {fd:11d}  {td:12d}  {fd + td:6d}")
print(f"{'total':19s} {P['field']:4d}  {P['field_decision']:11d}"
      f"  {P['table_decision']:12d}  {P['invented_decision']:6d}")
print()
print("facts the conceptual model states:", CONCEPTUAL_FACTS)
print("decisions the physical schema invents:", P["invented_decision"])
print("ratio:", round(P["invented_decision"] / CONCEPTUAL_FACTS, 4))
print()
print("even if every decision has only two options, the number of physical schemas")
print("corresponding to the same logical model is:", 2 ** P["invented_decision"])
print("digit count:", len(str(2 ** P["invented_decision"])))
```

```
table          fields  field decisions  table decisions  total
WorkOrder              5           20             2      22
Technician             3           12             2      14
Part                   4           16             2      18
Machine                3           12             2      14
Document               4           16             2      18
WorkOrder_Part         3           12             2      14
Technician_Machine     3           12             2      14
total                 25          100            14     114

facts the conceptual model states: 26
decisions the physical schema invents: 114
ratio: 4.3846

even if every decision has only two options, the number of physical schemas
corresponding to the same logical model is: 20769187434139310514121985316880384
digit count: 35
```

**System:** the conceptual model states 26 facts. **Notation:** the physical schema
carries 7 tables and 25 fields. **Cost:** writing this schema requires giving **114
decisions**, and none of them is written in the conceptual model. The ratio is
**4.3846**, meaning roughly **4.4 times** as many decisions as the model states are
given outside the model.

The last line is this number's counterpart in the notation's own language. If a
decision offers at least two options — nullable or not, yes or no — the number of
physical schemas corresponding to the same logical model is **two to the power of
one hundred fourteen**, a thirty-five-digit number. The real number of options is
larger than this, because type and length take more than two values. **The logical
model fits all of this set of schemas at once** and prefers none of them over
another.

- **VM16** — The lower-bound count assumes every decision has only two options; the
  real number of options varies by storage product and is therefore not written.

#### Who Owns the Decision

The one hundred fourteen decisions do not disappear; only **their owner changes**. A
decision has three possible owners. The model may have stated it as a fact. The
decision may be **derivable** from another fact. Or a person, while writing code or
building the schema, answers according to their own knowledge.

The distinction between these three is measurable, and the measurement shows two
separate behaviors.

- **VM17** — A decision's owner is one of three: a fact written in the model, a
  result derived from another fact, or a person. When counted as a model fact, the
  conceptual model's fact count grows; when counted as a derived decision, it does
  not.

```python
"""Who owns the 114 decisions: carrying to the model and deriving from a rule are separate jobs."""
FIELD_DECISION = ["type", "length", "nullability", "default value"]
TABLE_DECISION = ["primary key index", "character set"]
LOGICAL = {
    "WorkOrder": ["key", "number", "opened", "status", "Technician_key"],
    "Technician": ["key", "name", "specialty"],
    "Part": ["key", "code", "name", "unit"],
    "Machine": ["key", "code", "type"],
    "Document": ["key", "type", "date", "WorkOrder_key"],
    "WorkOrder_Part": ["key", "WorkOrder_key", "Part_key"],
    "Technician_Machine": ["key", "Technician_key", "Machine_key"],
}
ATTRIBUTES = {"WorkOrder": ["number", "opened", "status"], "Technician": ["name", "specialty"],
              "Part": ["code", "name", "unit"], "Machine": ["code", "type"],
              "Document": ["type", "date"]}

DECISIONS = [(t, a, k) for t, fields in LOGICAL.items() for a in fields
             for k in FIELD_DECISION]
DECISIONS += [(t, None, k) for t in LOGICAL for k in TABLE_DECISION]


def owner(decision, settings):
    t, a, k = decision
    is_conceptual = a in ATTRIBUTES.get(t, [])
    if settings["field"] and is_conceptual and k in ("type", "length"):
        return "model"
    if settings["requiredness"] and is_conceptual and k == "nullability":
        return "model"
    if settings["foreign"] and a is not None and a.endswith("_key") \
            and k in ("type", "length"):
        return "derived"
    if settings["integrity"] and a == "key" and k == "nullability":
        return "derived"
    return "person"


SETTINGS = [
    ("conceptual model as is", dict(field=0, requiredness=0, foreign=0, integrity=0)),
    ("+ field for 12 attributes", dict(field=1, requiredness=0, foreign=0, integrity=0)),
    ("+ requiredness for 12 attributes", dict(field=1, requiredness=1, foreign=0, integrity=0)),
    ("+ foreign key derivation", dict(field=1, requiredness=1, foreign=1, integrity=0)),
    ("+ entity integrity", dict(field=1, requiredness=1, foreign=1, integrity=1)),
]
print("total decisions:", len(DECISIONS))
print()
print("configuration                      in model  derived  person  model facts  total")
for name, settings in SETTINGS:
    tally = {"model": 0, "derived": 0, "person": 0}
    for d in DECISIONS:
        tally[owner(d, settings)] += 1
    facts = 26 + tally["model"]
    print(f"{name:34s}  {tally['model']:7d}  {tally['derived']:10d}  {tally['person']:4d}"
          f"  {facts:12d}  {facts + tally['person']:6d}")
```

```
total decisions: 114

configuration                      in model  derived  person  model facts  total
conceptual model as is                    0           0   114            26     140
+ field for 12 attributes                24           0    90            50     140
+ requiredness for 12 attributes         36           0    78            62     140
+ foreign key derivation                 36          12    66            62     128
+ entity integrity                       36          19    59            62     121
```

The table's last column is this lesson's most instructive number. In the first three
rows the total stands **fixed at 140**: the facts the model carries and the
decisions a person makes trade against each other **one for one**. Adding the type
and length information of the twelve attributes to the conceptual model raises the
fact count from 26 to 50 and lowers the decisions left to a person from 114 to 90.
When requiredness information is added as well, facts reach 62, and the decisions
left to a person reach 78. **Enriching the model does not reduce the number of
decisions, it changes who owns the decision** — and this is a good trade, because a
decision in the model is written, readable, and discussable.

The last two rows do something different, and they **lower** the total. The type and
length of a foreign key field can be **derived** from the primary key it refers to;
and further, the field forming a primary key cannot be null — this is a direct
consequence of the **entity integrity** established in the Data Modeling and
Relational Theory course. With these two derivations, the decisions left to a person
drop from 78 to **59**, and the total drops from 140 to **121**. Derivation is
different from trading: it genuinely reduces the number of decisions without
enlarging the model, because it makes stating the same information twice
unnecessary.

#### The Model's Silence Makes a Decision Invisible

Each of the remaining fifty-nine decisions is a person's answer, and this answer
leaves no trace in the model. The consequence of this is not that the decision is
wrong; it is that **wrongness cannot be shown.** Looking at a single field is
enough.

- **VM18** — The part code pool has eight codes, and only the first five had been
  seen at decision time.

```python
"""A single invented decision: the length of the Part.code field. The model rules out no option."""
CODE = ["MK-01", "MK-002", "TR-08", "PL-7", "GK-1", "RD-1140", "HD-220-A", "SB-33012"]

print("length  rejected codes  which ones")
for length in range(4, 11):
    rejected = [k for k in CODE if len(k) > length]
    print(f"{length:6d}  {len(rejected):14d}  {', '.join(rejected) if rejected else '-'}")
print()
print("facts the conceptual model states about this decision: 0")
print("smallest safe length read from the data:", max(len(k) for k in CODE))
print()
seen = CODE[:5]
choice = max(len(k) for k in seen)
later = [k for k in CODE if len(k) > choice]
print("codes seen at decision time:", len(seen), "| chosen length:", choice)
print("codes arriving later:", len(CODE) - len(seen),
      "| rejected:", len(later), "->", ", ".join(later))
```

```
length  rejected codes  which ones
     4               6  MK-01, MK-002, TR-08, RD-1140, HD-220-A, SB-33012
     5               4  MK-002, RD-1140, HD-220-A, SB-33012
     6               3  RD-1140, HD-220-A, SB-33012
     7               2  HD-220-A, SB-33012
     8               0  -
     9               0  -
    10               0  -

facts the conceptual model states about this decision: 0
smallest safe length read from the data: 8

codes seen at decision time: 5 | chosen length: 6
codes arriving later: 3 | rejected: 3 -> RD-1140, HD-220-A, SB-33012
```

Of the one hundred fourteen decisions, this is just one: the length of the
`Part.code` field. The conceptual model states **zero facts** about this decision;
it states that the part code is unique, not how many characters it has. Because of
this, every option between 4 and 10 is **equally suited** to the model, and none can
be ruled out by looking at the model.

The only thing that rules out a decision is data, and the data is incomplete at
decision time. The person making the decision saw five codes, the longest being six
characters, and chose a length of 6. All **three** of the codes that arrive later
are rejected. This is not a mistake, it is a **guess** — and the fact that it is a
guess is not written in the model, because this row does not exist in the model at
all. Five years later, someone looking at that field cannot tell whether the number
6 is a domain rule or one day's guess.

From this follows the data-modeling counterpart of the course's rule: **every place
the model is silent is a place of decision, and the decision made there cannot be
discussed, because it does not appear in the model.** The number one hundred
fourteen is therefore not a measure of schema size but a measure of
**invisibility**.

#### Summary

- The physical schema requires four decisions per field and two per table. Seven
  tables and twenty-five fields produce 100 field decisions and 14 table decisions,
  a total of **114 decisions**.
- The conceptual model states 26 facts; the physical schema invents 114 decisions.
  The ratio is 4.3846, roughly 4.4 times what the model states.
- Even if every decision has only two options, the number of physical schemas
  corresponding to the same logical model is 2 to the power of 114, a thirty-five-
  digit number; the logical model fits all of them at once.
- Carrying a decision into the model is a **trade** and does not change the total:
  when field and requiredness information is added for 12 attributes, facts rise
  from 26 to 62 while the decisions left to a person fall from 114 to 78, and the
  total stays fixed at 140.
- **Deriving** a decision from a rule genuinely lowers the total: when the foreign
  key's type comes from the key it refers to, and the key's non-nullability comes
  from entity integrity, the decisions left to a person become 59 and the total
  becomes 121.
- A decision the model is silent about becomes invisible: the model states zero
  facts about the length of `Part.code`, the five codes seen at decision time lead
  to a chosen length of 6, and all three codes arriving later are rejected.

#### Course Wrap-Up

Ten lessons modeled a single maintenance shop and asked a single question: how many
other systems can a notation not distinguish the system from. That was the course's
rule — **a notation that does not state how many systems it cannot tell apart is
counted as unmeasured.** In every lesson, three numbers stood side by side: the
system's real fact count, the number of symbols the notation writes, and the cost
between them.

| Lesson | System | Notation | Cost |
|---|---|---|---|
| Notation Family and Loss | 5 associations, 11 hidden fields | 20 symbols | **2048** systems fall onto the same diagram |
| Class Diagrams | the same 5 associations | 25 and 31 symbols | **64** systems remain at 25 symbols, **1** at 31 |
| Component and Deployment Diagrams | 6 classes, **203** partitions; 540 covering assignments | 3 boxes + 2 links | the crudest drawing is consistent with **10 partitions**; link symbols reduce 540 to 20, content symbols to 1 |
| Use Case Diagrams | 20 facts: 2 actors, 3 scenarios, 5 associations, 3 ownerships, 7 internal steps | 9 symbols | **512** systems fall onto the same diagram; baseline ambiguity 128 |
| Sequence Diagrams | 24 traces | 1 diagram, 1 trace | coverage **1/24 = 0.0417**; four diagrams 0.1667 |
| Activity Diagrams | 10 tasks, 13 prerequisites | 23 symbols, 1 partial order | **168 total orders** are accepted, a single order's coverage is 0.006; a decision node carries it to 492 |
| State Machine Diagrams | 6 real sequences | 10 symbols (4 states, 6 transitions) | under-acceptance **2**, over-acceptance **2**; adding two transitions gives 0 and **26** — thirteenfold |
| The Entity–Relationship Model | 26 facts | 26 written items | **1,048,576** systems fall onto the same notation |
| Conceptual, Logical, and Physical Model | 26 facts | 7 tables, 25 fields | **2 dropped constraints**, 13 invented fields, 175 over-acceptances |
| From Model to Schema | 26 facts | 7 tables, 25 fields | **114 invented decisions**, 4.4 times what the model states |

A second reading of the table is more valuable than the first: **fixing a notation
can make it more wrong.** Adding two transitions to the state machine zeroed out the
rejected sequence but raised over-acceptance thirteenfold. Demoting a fact from
entity to attribute simplified the model but made eleven states unrepresentable.
Rescuing a dropped constraint required adding a field not present in the model at
all. Adding detail is not always a gain, and it **is not added without counting the
loss.**

The third and most expensive result came from data modeling: **a level of
abstraction both forgets and invents, and the two are counted separately.** On the
way down from the conceptual model to the physical schema, two of the five
constraints dropped, but 114 decisions appeared that never were written in the
model. The two forgotten constraints were visible, because they had once been
written in the model; the 114 invented decisions are invisible, because they were
never written at all. **The most dangerous thing about a notation is not what it
erases, but what it never states.**

No diagram was drawn throughout the course. Every notation was built and counted as
a list of the information items it carries; what was measured was not the picture's
beauty, but **how many systems the picture collapsed onto one picture**. Which
diagram type suits which question, and what it costs to keep a set of diagrams
current, were not this course's questions; both were measured in the Software
Architecture curriculum's Architectural Decisions and Documentation course and were
not carried over here.

The next course, **Advanced Algorithms and Problem Solving**, carries the same
discipline forward on a different notation. There, reducing a problem to a
recognized algorithmic pattern is taught — divide and conquer, greedy choice,
dynamic programming, backtracking. Reduction is itself a choice of notation: fitting
a problem into a pattern makes some of its aspects invisible, and if the pattern's
conditions are not met, the answer comes out silently wrong. This is why a
justification stands at the center of that course: **showing that** the
optimal-substructure and overlapping-subproblem conditions **are satisfied**. The
rule here holds there too — until what a reduction drops is written down, the
reduction is counted as unmeasured.

