Python Part II · Module 28 · Report Parsing
Parsing Implementation and Utilisation Reports
Place-and-route output is the third document shape in this module and the technique for reading it was already built in Chapter 28.1: a section header opens a record, the lines underneath belong to it, and the next header closes it. What is new is the document rather than the method — percentages that look like they carry a unit and do not, power figures whose unit changes per line, and section headers that appear twice because the tool reports the same thing before and after routing. The last of those breaks the obvious data structure in a way nothing complains about.
Advanced18 min readPythonPart IIAutomationPlace and RouteReport Parsing
Part II · Module 28 · Chapter 28.4 · Parsing Implementation and Utilisation Reports
1. A Third Document Shape
SHAPES = [
("simulation log", "a stream of events", "Module 20", "match lines as they go past"),
("timing report", "repeated multi-line records", "Chapter 28.1", "a record per path"),
("synthesis report", "one fact per line", "Chapter 28.3", "label, whitespace, number"),
("P&R report", "named sections of mixed content", "this chapter", "a record per section"),
]
print(f"{'document':<20} {'shape':<34} {'built in':<14} how to read it")
for name, shape, where, how in SHAPES:
print(f"{name:<20} {shape:<34} {where:<14} {how}")
print()
print("techniques needed that are new:", 0)
print("what is new :", "the vocabulary, and three traps in it")document shape built in how to read it
simulation log a stream of events Module 20 match lines as they go past
timing report repeated multi-line records Chapter 28.1 a record per path
synthesis report one fact per line Chapter 28.3 label, whitespace, number
P&R report named sections of mixed content this chapter a record per section
techniques needed that are new: 0
what is new : the vocabulary, and three traps in itThe canonical annotation for this chapter is a comment rather than code:
# same section-state pattern as the STA path reportThat is accurate and it is the whole point. Chapter 28.1 Section 2 built the machinery — accumulate into a record, close it when the next header arrives, append the last one after the loop — and a P&R report needs the same loop with a different header pattern.
What is worth the chapter is the three things about the content that the machinery does not protect you from.
2. The Section Loop
The specimen is constructed in the shape these tools emit — no place-and-route tool is installed here, per Chapter 23.1 Section 1:
=== Utilisation ===
Sites used: 182341
Sites available: 214000
Utilisation: 85.21%
=== Congestion ===
Layer Overflow
M2 0
M3 47
M4 312
=== Power ===
Internal: 142.331 mW
Switching: 88.902 mW
Leakage: 3.114 mW
Total: 234.347 mWimport re
from pathlib import Path
HEADER = re.compile(r"^===\s*(.+?)\s*===$")
def split_sections(path):
"""A list of (name, lines) — order preserved, duplicates kept."""
sections, name, body = [], None, []
for line in Path(path).read_text(encoding="utf-8").splitlines():
m = HEADER.match(line)
if m:
if name is not None:
sections.append((name, body))
name, body = m.group(1), []
continue
if name is not None and line.strip():
body.append(line)
if name is not None:
sections.append((name, body))
return sections
for name, body in split_sections("impl.rpt"):
print(f"{name:<14} {len(body)} line(s)")
print()
print("sections:", len(split_sections("impl.rpt")))
print("the loop is Chapter 28.1's:", "header opens, next header closes, last appended after")Utilisation 3 line(s)
Congestion 4 line(s)
Power 4 line(s)
sections: 3
the loop is Chapter 28.1's: header opens, next header closes, last appended afterThree sections, and the function returns a list of pairs rather than a dictionary. That looks like an odd choice on this specimen, where every name is unique, and the next section is why.
3. Section Names Repeat
=== Utilisation ===
Sites used: 182341
Utilisation: 85.21%
=== Congestion ===
M3 47
=== Utilisation ===
Sites used: 189022
Utilisation: 88.33%import re
from pathlib import Path
HEADER = re.compile(r"^===\s*(.+?)\s*===$")
lines = Path("impl2.rpt").read_text(encoding="utf-8").splitlines()
as_dict, name = {}, None
for line in lines:
m = HEADER.match(line)
if m:
name = m.group(1)
as_dict[name] = []
continue
if name and line.strip():
as_dict[name].append(line)
as_list, name, body = [], None, []
for line in lines:
m = HEADER.match(line)
if m:
if name is not None:
as_list.append((name, body))
name, body = m.group(1), []
continue
if name is not None and line.strip():
body.append(line)
if name is not None:
as_list.append((name, body))
print("headers in the file:", sum(1 for l in lines if HEADER.match(l)))
print()
print("as a dict :", {k: len(v) for k, v in as_dict.items()})
print("as a list :", [(k, len(v)) for k, v in as_list])
print()
print("dict entries:", len(as_dict))
print("list entries:", len(as_list))
print("what the dict lost:", "the first Utilisation section, entirely")
print("the dict's Utilisation says:", as_dict["Utilisation"][-1].strip())headers in the file: 3
as a dict : {'Utilisation': 2, 'Congestion': 1}
as a list : [('Utilisation', 2), ('Congestion', 1), ('Utilisation', 2)]
dict entries: 2
list entries: 3
what the dict lost: the first Utilisation section, entirely
the dict's Utilisation says: Utilisation: 88.33%Three sections in, two out, and nothing complained.
A place-and-route report emits the same section before and after routing — utilisation before and after filler insertion, congestion before and after optimisation, timing at every stage. Keying by section name overwrites, and the value that survives is whichever came last, which is usually the one you wanted and gives no indication when it is not.
The loss is worse than one section, because the pre-to-post difference is the most useful thing in the document:
SECTIONS = [("Utilisation", 85.21), ("Congestion", None), ("Utilisation", 88.33)]
util = [(i, v) for i, (name, v) in enumerate(SECTIONS) if name == "Utilisation"]
print("Utilisation sections:", len(util))
for i, v in util:
print(f" position {i}: {v}%")
print()
pre, post = util[0][1], util[-1][1]
print("pre-route :", f"{pre}%")
print("post-route:", f"{post}%")
print("the increase:", f"{post - pre:+.2f} points")
print()
print("what the increase is :", "filler cells and routing-driven placement")
print("what a dict would show:", f"{post}% and nothing to compare it to")Utilisation sections: 2
position 0: 85.21%
position 2: 88.33%
pre-route : 85.21%
post-route: 88.33%
the increase: +3.12 points
what the increase is : filler cells and routing-driven placement
what a dict would show: 88.33% and nothing to compare it toA list of pairs keeps the order and the duplicates, and the position in the list is what distinguishes pre from post when the tool does not label them.
Where a tool does label them — === Utilisation (post-route) === — a dict works, and relying on that is a bet on the label never changing. The list costs nothing and does not take the bet.
4. A Percentage Carries No Unit
VALUES = ["85.21%", "85.21", "0.8521"]
for text in VALUES:
parsed = float(text.rstrip("%"))
print(f"{text:<9} float() -> {parsed}")
print()
THRESHOLD = 0.90
for text in VALUES:
parsed = float(text.rstrip("%"))
print(f"{text:<9} parsed {parsed:<8} > {THRESHOLD}? {parsed > THRESHOLD}")
print()
print("all three mean the same utilisation:", True)
print("a threshold of 0.90 against 85.21 :", "never fires — 85.21 is always greater")
print("a threshold of 90 against 0.8521 :", "never fires either")85.21% float() -> 85.21
85.21 float() -> 85.21
0.8521 float() -> 0.8521
85.21% parsed 85.21 > 0.9? True
85.21 parsed 85.21 > 0.9? True
0.8521 parsed 0.8521 > 0.9? False
all three mean the same utilisation: True
a threshold of 0.90 against 85.21 : never fires — 85.21 is always greater
a threshold of 90 against 0.8521 : never fires eitherThree spellings of 85.21% utilisation, and a 0.90 threshold gives two different answers.
This is Chapter 28.1 Section 8's unit problem in the form people actually meet it, and it is nastier there because a percentage looks like it carries its own unit. Stripping the % throws away the only thing that distinguished the two conventions, and the resulting bare float is as ambiguous as a bare slack.
The failure is also directional, which makes it worse. A utilisation check written as if util > 0.90: warn() against values parsed as percentages fires on every run — 85.21 is greater than 0.90 — so the warning is always on, gets ignored, and then means nothing when utilisation genuinely reaches 95%.
def parse_percent(text):
"""Return a fraction in [0, 1], whatever the report's convention."""
raw = text.strip()
had_sign = raw.endswith("%")
value = float(raw.rstrip("%"))
if had_sign:
return value / 100.0
if value > 1.0:
return value / 100.0
return value
for text in ("85.21%", "85.21", "0.8521", "100%", "1.0", "0.5"):
print(f"{text:<9} -> {parse_percent(text):.4f}")
print()
print("ambiguous case:", "1.0")
print("read as :", f"{parse_percent('1.0'):.4f}", "— 100%, not 1%")
print("why :", "a value <= 1 with no sign is assumed to be a fraction")
print("the honest fix :", "record the convention, do not infer it")85.21% -> 0.8521
85.21 -> 0.8521
0.8521 -> 0.8521
100% -> 1.0000
1.0 -> 1.0000
0.5 -> 0.5000
ambiguous case: 1.0
read as : 1.0000 — 100%, not 1%
why : a value <= 1 with no sign is assumed to be a fraction
the honest fix : record the convention, do not infer itparse_percent normalises all six, and the last three lines are the honest caveat: 1.0 is genuinely ambiguous — one percent or one hundred percent — and the heuristic picks the common reading rather than the certain one.
A design at 1% utilisation is unusual and a design at 100% is a real state, so the heuristic is right far more often than not. It is still a heuristic, and the better answer is to record which convention the report used, once, where the parser was written for that tool.
5. Congestion Is a Table
=== Congestion ===
Layer Overflow Tracks
M1 0 41200
M2 0 41200
M3 47 38600
M4 312 38600
M5 0 22100import re
from pathlib import Path
ROW = re.compile(r"^\s*(M\d+)\s+(\d+)\s+(\d+)\s*$")
rows = []
for line in Path("cong.rpt").read_text(encoding="utf-8").splitlines():
m = ROW.match(line)
if m:
rows.append({"layer": m.group(1), "overflow": int(m.group(2)),
"tracks": int(m.group(3))})
for r in rows:
flag = " <-- congested" if r["overflow"] else ""
print(f"{r['layer']:<4} overflow {r['overflow']:>5} tracks {r['tracks']:>6}{flag}")
congested = [r for r in rows if r["overflow"] > 0]
print()
print("layers :", len(rows))
print("congested layers:", len(congested), [r["layer"] for r in congested])
print("total overflow :", sum(r["overflow"] for r in rows))
print("worst layer :", max(rows, key=lambda r: r["overflow"])["layer"])M1 overflow 0 tracks 41200
M2 overflow 0 tracks 41200
M3 overflow 47 tracks 38600 <-- congested
M4 overflow 312 tracks 38600 <-- congested
M5 overflow 0 tracks 22100
layers : 5
congested layers: 2 ['M3', 'M4']
total overflow : 359
worst layer : M4A table inside a section, which is the third structure in this module after multi-line records and label-value lines. The row regex is the schema — three columns, anchored at both ends — and a report with a fourth column simply stops matching, which the row count catches.
Two things about the numbers are worth noting, and they mirror Chapter 28.2 exactly.
The total overflow is a TNS-shaped number: a sum that says how much congestion there is and nothing about where. The worst layer is a WNS-shaped number: where the worst of it is, and nothing about how much. Reporting both, plus the count of congested layers, is the same triple and for the same reason.
Anchoring the row pattern with ^\s* and \s*$ matters more here than elsewhere. An unanchored (M\d+)\s+(\d+)\s+(\d+) would happily match inside a prose line mentioning M3, and a congestion table is surrounded by prose.
6. Power Has a Unit Per Line
import re
LINES = [
" Internal: 142.331 mW",
" Switching: 88.902 mW",
" Leakage: 3114.000 uW",
" Total: 0.234 W",
]
FIELD = re.compile(r"^\s*(\w+):\s*([-+]?[\d,]*\.?\d+)\s*([munp]?W)\s*$")
SCALE = {"W": 1.0, "mW": 1e-3, "uW": 1e-6, "nW": 1e-9, "pW": 1e-12}
parsed = {}
for line in LINES:
m = FIELD.match(line)
name, value, unit = m.group(1).lower(), float(m.group(2).replace(",", "")), m.group(3)
parsed[name] = value * SCALE[unit]
print(f"{name:<10} {value:>10} {unit:<3} -> {parsed[name]:.9f} W")
print()
parts = parsed["internal"] + parsed["switching"] + parsed["leakage"]
print("parts sum to:", f"{parts:.9f} W")
print("total says :", f"{parsed['total']:.9f} W")
print("agree to 1uW:", abs(parts - parsed["total"]) < 1e-6)
print()
print("units in this report:", sorted({m.group(3) for m in (FIELD.match(l) for l in LINES)}))
print("fields parsed in W :", len(parsed))internal 142.331 mW -> 0.142331000 W
switching 88.902 mW -> 0.088902000 W
leakage 3114.0 uW -> 0.003114000 W
total 0.234 W -> 0.234000000 W
parts sum to: 0.234347000 W
total says : 0.234000000 W
agree to 1uW: False
units in this report: ['W', 'mW', 'uW']
fields parsed in W : 4Three different units in four lines, which is normal: a tool picks a unit per field so the mantissa stays readable, and leakage is often orders of magnitude below the rest.
The cross-check disagrees, and that is the example working correctly. The parts sum to 234.347 mW and the total line says 0.234 W — the total was rounded to three decimal places in watts, which is 347 microwatts of apparent discrepancy from rounding alone.
That is a real and common situation, and it means the tolerance has to come from the reported precision rather than from a constant:
PARTS_W = 0.234347
TOTAL_W = 0.234
TOTAL_TEXT = "0.234"
decimals = len(TOTAL_TEXT.split(".")[1])
tolerance = 0.5 * 10 ** -decimals
print("parts :", PARTS_W)
print("total text:", TOTAL_TEXT, f"({decimals} decimals)")
print("tolerance :", tolerance)
print()
print("difference:", round(abs(PARTS_W - TOTAL_W), 9))
print("within the reported precision:", abs(PARTS_W - TOTAL_W) <= tolerance)
print()
print("a fixed 1e-6 tolerance would say:", abs(PARTS_W - TOTAL_W) <= 1e-6)
print("and would be wrong because :", "the total was printed to 3 decimals, not 9")parts : 0.234347
total text: 0.234 (3 decimals)
tolerance : 0.0005
difference: 0.000347
within the reported precision: True
a fixed 1e-6 tolerance would say: False
and would be wrong because : the total was printed to 3 decimals, not 9Derive the tolerance from the printed precision. Half of the last decimal place is the most the rounding can have cost, and a cross-check with that tolerance distinguishes a rounding artefact from a genuine mismatch — where a fixed 1e-6 flags every report.
Chapter 28.1 Section 7's rule about never comparing floats for equality has this as its practical form: the tolerance is a property of the document, not of floating point.
7. A Global Number Hides a Local Problem
REGIONS = [
("core_nw", 0.61), ("core_ne", 0.58), ("core_sw", 0.97),
("core_se", 0.63), ("periph_n", 0.44), ("periph_s", 0.47),
]
overall = sum(u for _, u in REGIONS) / len(REGIONS)
print(f"{'region':<12} {'utilisation':>12}")
for name, util in REGIONS:
flag = " <-- full" if util > 0.90 else ""
print(f"{name:<12} {util:>11.2%}{flag}")
print()
print("overall (mean) :", f"{overall:.2%}")
print("worst region :", max(REGIONS, key=lambda r: r[1])[0],
f"{max(u for _, u in REGIONS):.2%}")
print("spread :", f"{max(u for _, u in REGIONS) - min(u for _, u in REGIONS):.2%}")
print()
print("a 90% threshold on the overall number fires:", overall > 0.90)
print("a 90% threshold on the worst region fires :", max(u for _, u in REGIONS) > 0.90)region utilisation
core_nw 61.00%
core_ne 58.00%
core_sw 97.00% <-- full
core_se 63.00%
periph_n 44.00%
periph_s 47.00%
overall (mean) : 61.67%
worst region : core_sw 97.00%
spread : 53.00%
a 90% threshold on the overall number fires: False
a 90% threshold on the worst region fires : TrueAn overall utilisation of 61.67% and one region at 97%.
The design has a place where nothing more will fit, and the headline figure says it is comfortably half empty. This is Chapter 28.2 Section 7's clock-group argument arriving in space rather than in time: a single number averaged over a design names no location, and the location is where the problem is.
The relationship to congestion is direct:
REGIONS = {"core_nw": 0.61, "core_ne": 0.58, "core_sw": 0.97,
"core_se": 0.63, "periph_n": 0.44, "periph_s": 0.47}
OVERFLOW = {"core_nw": 0, "core_ne": 0, "core_sw": 312,
"core_se": 47, "periph_n": 0, "periph_s": 0}
print(f"{'region':<12} {'util':>8} {'overflow':>9}")
for name in REGIONS:
print(f"{name:<12} {REGIONS[name]:>7.0%} {OVERFLOW[name]:>9}")
congested = [n for n in REGIONS if OVERFLOW[n] > 0]
full = [n for n in REGIONS if REGIONS[n] > 0.90]
print()
print("congested regions:", congested)
print("full regions :", full)
print("the overlap :", sorted(set(congested) & set(full)))
print()
print("total overflow :", sum(OVERFLOW.values()))
print("in the full region:", OVERFLOW["core_sw"],
f"({OVERFLOW['core_sw'] / sum(OVERFLOW.values()):.0%})")region util overflow
core_nw 61% 0
core_ne 58% 0
core_sw 97% 312
core_se 63% 47
periph_n 44% 0
periph_s 47% 0
congested regions: ['core_sw', 'core_se']
full regions : ['core_sw']
the overlap : ['core_sw']
total overflow : 359
in the full region: 312 (87%)87% of the congestion is in the one region that is 97% full, which is the finding a global utilisation figure cannot produce.
Two things follow for the record this chapter builds.
Report the worst region, not the mean. Chapter 28.2's WNS-shaped number applies here too: the worst region is what gates, and the mean is what reassures.
Keep the per-region figures, not just the aggregate. Chapter 28.5 compares two runs, and a run where the mean held steady while one region went from 80% to 97% is the one worth catching:
BEFORE = {"core_nw": 0.61, "core_sw": 0.80, "core_se": 0.63, "periph_n": 0.44}
AFTER = {"core_nw": 0.58, "core_sw": 0.97, "core_se": 0.60, "periph_n": 0.44}
mean_before = sum(BEFORE.values()) / len(BEFORE)
mean_after = sum(AFTER.values()) / len(AFTER)
print("mean before:", f"{mean_before:.2%}")
print("mean after :", f"{mean_after:.2%}")
print("mean delta :", f"{mean_after - mean_before:+.2%}")
print()
print(f"{'region':<12} {'before':>8} {'after':>8} {'delta':>9}")
for name in BEFORE:
d = AFTER[name] - BEFORE[name]
print(f"{name:<12} {BEFORE[name]:>7.0%} {AFTER[name]:>7.0%} {d:>+8.1%}")
print()
print("worst region before:", f"{max(BEFORE.values()):.0%}")
print("worst region after :", f"{max(AFTER.values()):.0%}")
print("the mean moved by :", f"{mean_after - mean_before:+.2%}")
print("the worst moved by :", f"{max(AFTER.values()) - max(BEFORE.values()):+.0%}")mean before: 62.00%
mean after : 64.75%
mean delta : +2.75%
region before after delta
core_nw 61% 58% -3.0%
core_sw 80% 97% +17.0%
core_se 63% 60% -3.0%
periph_n 44% 44% +0.0%
worst region before: 80%
worst region after : 97%
the mean moved by : +2.75%
the worst moved by : +17%The mean moved 2.75 points and the worst region moved 17.
A QoR gate watching the mean would see a small, unremarkable change; one watching the maximum sees a region that has gone from comfortable to full. Chapter 28.6 thresholds on exactly this kind of figure, and which figure it thresholds on decides whether it catches the run.
8. The Parser, Assembled
=== Utilisation ===
Sites used: 182341
Sites available: 214000
Utilisation: 85.21%
=== Congestion ===
Layer Overflow Tracks
M3 47 38600
M4 312 38600
=== Power ===
Internal: 142.331 mW
Switching: 88.902 mW
Leakage: 3114.000 uW
Total: 0.234 W
=== Utilisation ===
Sites used: 189022
Sites available: 214000
Utilisation: 88.33%import json
import re
from pathlib import Path
HEADER = re.compile(r"^===\s*(.+?)\s*===$")
LABELLED = re.compile(r"^\s*([\w /]+?):\s*([-+]?[\d,]*\.?\d+)\s*(%|[munp]?W)?\s*$")
CONG_ROW = re.compile(r"^\s*(M\d+)\s+(\d+)\s+(\d+)\s*$")
SCALE = {"W": 1.0, "mW": 1e-3, "uW": 1e-6, "nW": 1e-9, "pW": 1e-12}
def parse_percent(text, had_sign):
value = float(text.replace(",", ""))
return value / 100.0 if had_sign or value > 1.0 else value
def split_sections(text):
out, name, body = [], None, []
for line in text.splitlines():
m = HEADER.match(line)
if m:
if name is not None:
out.append((name, body))
name, body = m.group(1), []
continue
if name is not None and line.strip():
body.append(line)
if name is not None:
out.append((name, body))
return out
def parse_impl(path):
sections = split_sections(Path(path).read_text(encoding="utf-8"))
utilisation, congestion, power, problems = [], [], {}, []
for name, body in sections:
if name.startswith("Utilisation"):
fields = {}
for line in body:
m = LABELLED.match(line)
if not m:
continue
key, value, unit = m.group(1).strip().lower(), m.group(2), m.group(3)
fields[key] = parse_percent(value, unit == "%") if key == "utilisation" \
else float(value.replace(",", ""))
utilisation.append(fields)
elif name.startswith("Congestion"):
for line in body:
m = CONG_ROW.match(line)
if m:
congestion.append({"layer": m.group(1), "overflow": int(m.group(2)),
"tracks": int(m.group(3))})
elif name.startswith("Power"):
for line in body:
m = LABELLED.match(line)
if m and m.group(3) in SCALE:
power[m.group(1).strip().lower()] = \
float(m.group(2).replace(",", "")) * SCALE[m.group(3)]
power[m.group(1).strip().lower() + "_text"] = m.group(2)
if {"internal", "switching", "leakage", "total"} <= set(power):
parts = power["internal"] + power["switching"] + power["leakage"]
decimals = len(power["total_text"].split(".")[-1])
if abs(parts - power["total"]) > 0.5 * 10 ** -decimals:
problems.append(f"power parts {parts} != total {power['total']}")
if not utilisation:
problems.append("no Utilisation section found")
return {"utilisation_sections": len(utilisation),
"utilisation_pre": utilisation[0].get("utilisation") if utilisation else None,
"utilisation_post": utilisation[-1].get("utilisation") if utilisation else None,
"congested_layers": sorted(r["layer"] for r in congestion if r["overflow"]),
"total_overflow": sum(r["overflow"] for r in congestion),
"power_total_w": round(power.get("total", 0.0), 9),
"sections": [n for n, _ in sections],
"problems": problems}
result = parse_impl("impl.rpt")
for key in ("sections", "utilisation_sections", "utilisation_pre", "utilisation_post",
"congested_layers", "total_overflow", "power_total_w", "problems"):
print(f"{key:<22} {result[key]}")
print()
pre, post = result["utilisation_pre"], result["utilisation_post"]
print("utilisation rose by:", f"{(post - pre) * 100:+.2f} points")
print("both sections kept :", result["utilisation_sections"] == 2)sections ['Utilisation', 'Congestion', 'Power', 'Utilisation']
utilisation_sections 2
utilisation_pre 0.8521
utilisation_post 0.8833
congested_layers ['M3', 'M4']
total_overflow 359
power_total_w 0.234
problems []
utilisation rose by: +3.12 points
both sections kept : TrueSix decisions, and the first two are the ones this chapter exists for.
sections is reported as a list including the duplicate, so a reader can see that Utilisation appeared twice. A record that showed only a count would hide the structure.
utilisation_pre and utilisation_post come from [0] and [-1], which is explicit about the assumption — first occurrence is pre-route, last is post — rather than letting a dict pick silently.
Utilisation is stored as a fraction, normalised by parse_percent, so Chapter 28.6's threshold can be written as 0.90 and mean ninety percent.
Power is stored in watts with the original text kept, so the cross-check can derive its tolerance from the printed precision.
The cross-check is a problem, not an exception, matching Chapter 28.3 Section 8's record shape.
A missing Utilisation section is also a problem, because an empty list would otherwise make [0] raise far from the cause.
9. Predict, Run, Observe
import re
HEADER = re.compile(r"^===\s*(.+?)\s*===$")
TEXT = "=== A ===\n x\n=== B ===\n y\n=== A ===\n z"
# A
d = {}
name = None
for line in TEXT.splitlines():
m = HEADER.match(line)
if m:
name = m.group(1)
d[name] = []
continue
if name:
d[name].append(line.strip())
print("A:", {k: v for k, v in d.items()})
# B
lst, name, body = [], None, []
for line in TEXT.splitlines():
m = HEADER.match(line)
if m:
if name:
lst.append((name, body))
name, body = m.group(1), []
continue
if name:
body.append(line.strip())
if name:
lst.append((name, body))
print("B:", lst)
# C
print("C:", len(d), len(lst))
# D
print("D:", float("85.21%".rstrip("%")), float("0.8521"))
# E
print("E:", float("85.21") > 0.90, float("0.8521") > 0.90)
# F
print("F:", 142.331e-3 + 88.902e-3 + 3114.0e-6)
# G
print("G:", 0.5 * 10 ** -len("0.234".split(".")[1]))
# H
print("H:", abs(0.234347 - 0.234) <= 0.0005, abs(0.234347 - 0.234) <= 1e-6)The verified output:
A: {'A': ['z'], 'B': ['y']}
B: [('A', ['x']), ('B', ['y']), ('A', ['z'])]
C: 2 3
D: 85.21 0.8521
E: True False
F: 0.23434700000000003
G: 0.0005
H: True FalseA — the dict kept two sections and A's first body is gone.
B — the list kept all three, in order.
C — two against three, from the same document.
D — stripping the % leaves two different numbers meaning the same thing.
E — and a 0.90 threshold answers differently for each.
F — three units add up once scaled, and the sum is 0.23434700000000003 rather than 0.234347. Chapter 28.1 Section 7's accumulation, arriving inside the very cross-check that needs a tolerance — which is why the tolerance is derived rather than assumed.
G — the tolerance derived from three printed decimals.
H — which accepts the rounding and a fixed 1e-6 does not.
10. Exercises
Exercise 1 — four shapes. Name the document shapes in this module and the technique each needs.
Exercise 2 — nothing new. Say what the canonical annotation for this chapter is pointing at.
Exercise 3 — the section loop. Implement it for your own tool's header format.
Exercise 4 — repeated headers. Find a section your P&R tool emits twice.
Exercise 5 — the silent loss. Parse it into a dict and show which occurrence survives.
Exercise 6 — what the loss costs. Say why the pre-to-post difference is the useful figure.
Exercise 7 — index afterwards. Build a mapping from the list that keeps every occurrence.
Exercise 8 — three spellings. Show 85.21%, 85.21 and 0.8521 against a 0.90 threshold.
Exercise 9 — always on. Describe what a permanently firing utilisation warning does to a team.
Exercise 10 — normalise. Implement parse_percent and run it over six inputs.
Exercise 11 — the ambiguity. Say what 1.0 means and why the heuristic picks what it picks.
Exercise 12 — record the convention. Say where that belongs, citing Chapter 28.3.
Exercise 13 — anchor the row. Show an unanchored congestion pattern matching inside a prose line.
Exercise 14 — WNS and TNS again. Map total overflow and worst layer onto Chapter 28.2's numbers.
Exercise 15 — three units. Parse a power section whose fields use different units and sum them.
Exercise 16 — the rounding gap. Compute the discrepancy your tool's printed precision can produce.
Exercise 17 — tolerance from the document. Derive one from the decimals and show it accepting the rounding.
Exercise 18 — a fixed tolerance. Say what 1e-6 would flag on every report.
Exercise 19 — keep the text. Say why the parser stores the original string alongside the float.
Exercise 20 — the mean hides it. Build a per-region utilisation table where the mean is comfortable and one region is full.
Exercise 21 — where the congestion is. Correlate overflow with regional utilisation for your own design.
Exercise 22 — worst, not mean. Say which Chapter 28.6 should threshold on and why.
Exercise 23 — which utilisation. Find out which figure your tool reports and whether fillers are counted.
**Exercise 24 — pre and post explicitly.**Exercise 20 — pre and post explicitly. Say what [0] and [-1] assume and how you would check it.
11. Summary
- A P&R report is named sections of mixed content, and Chapter 28.1's loop reads it unchanged — header opens, next header closes, last appended after.
- Section names repeat, because a tool reports the same thing before and after routing.
- A dict keyed by section name silently keeps only the last occurrence, and three sections become two with no complaint.
- The pre-to-post difference is the most useful figure in the document, and it is exactly what the dict destroys.
- Parse into a list of
(name, lines)pairs and build any mapping deliberately afterwards. - A percentage looks like it carries its own unit and does not.
85.21%,85.21and0.8521are one value and three floats. - A
0.90threshold against values parsed as percentages fires on every run, so the warning is always on and therefore ignored. - Normalise to a fraction at the boundary, and accept that
1.0is genuinely ambiguous — record the convention rather than inferring it. - A congestion table needs an anchored row pattern, because the table is surrounded by prose mentioning layer names.
- Total overflow is TNS-shaped and the worst layer is WNS-shaped: report both and the count, exactly as Chapter 28.2 does.
- Power carries a unit per line, often three different ones, and must be normalised to a single unit before summing.
- Derive the cross-check tolerance from the printed precision — half the last decimal place — not from a constant.
- Keep the original text beside the parsed float so that tolerance can be computed at all.
- A global utilisation of 61.67% can contain a region at 97%, and the mean names no location.
- 87% of the congestion was in the one full region, which is the correlation a global figure cannot show.
- Report the worst region, not the mean — the same WNS-shaped argument as Chapter 28.2.
- Keep the per-region figures: a run where the mean moved 2.75 points and the worst moved 17 is the one to catch.
- Tools report several different utilisations and the labels are not standard, so record which one and refuse to compare across definitions.
- Report a failed cross-check as a problem, not an exception, and treat a missing section the same way.
The next chapter puts two of these records side by side and reports only the differences that mean something.
Where this fits
Part of the Python curriculum.
