diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py b/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
index 5031538..005eebc 100755
--- a/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
@@ -7,11 +7,17 @@
from __future__ import annotations
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
-def clean_text(text) -> str:
+MIN_BODY_LENGTH = 50
+MIN_ANSWER_LENGTH = 100
+
+
+def clean_text(text: str) -> str:
"""Clean text."""
if not text:
return ""
@@ -23,7 +29,7 @@ def clean_text(text) -> str:
return text.strip()
-def extract_cards(filepath) -> list[dict[str, str]]:
+def extract_cards(filepath: str) -> list[dict[str, str]]:
"""Extract cards."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
@@ -68,10 +74,10 @@ def extract_cards(filepath) -> list[dict[str, str]]:
content,
re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
- body = body.strip()
- if len(body) < 50:
+ for raw_header, raw_body in sections:
+ header = raw_header.strip()
+ body = raw_body.strip()
+ if len(body) < MIN_BODY_LENGTH:
continue
# Get first paragraph
@@ -102,8 +108,10 @@ def main() -> None:
for md_file in sorted(odpowiedzi_dir.glob("*.md")):
all_cards.extend(extract_cards(md_file))
- # APPROACH 1: Strict filtering - only cards with answer > 100 chars
- filtered_cards = [c for c in all_cards if len(c["back"]) > 100]
+ # APPROACH 1: Strict filtering - only cards with answer > threshold
+ filtered_cards = [
+ c for c in all_cards if len(c["back"]) > MIN_ANSWER_LENGTH
+ ]
# Remove duplicates
seen = set()
@@ -120,7 +128,11 @@ def main() -> None:
for c in unique:
f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
- print(f"✅ Approach 1 (Strict Filter): {len(unique)} cards -> {output_file.name}")
+ logger.info(
+ "Approach 1 (Strict Filter): %d cards -> %s",
+ len(unique),
+ output_file.name,
+ )
if __name__ == "__main__":
diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py b/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
index 98d459b..ad229ab 100755
--- a/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
@@ -7,11 +7,17 @@
from __future__ import annotations
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
-def clean_text(text) -> str:
+MIN_PARA_LENGTH = 30
+MIN_BODY_LENGTH = 50
+
+
+def clean_text(text: str) -> str:
"""Clean text."""
if not text:
return ""
@@ -23,7 +29,7 @@ def clean_text(text) -> str:
return text.strip()
-def extract_structured_content(body) -> str | None:
+def extract_structured_content(body: str) -> str | None:
"""Better extraction - look for multiple content types."""
parts = []
@@ -54,15 +60,14 @@ def extract_structured_content(body) -> str | None:
if p.strip()
and not p.startswith("```")
and not p.startswith("|")
- and len(p.strip()) > 30
+ and len(p.strip()) > MIN_PARA_LENGTH
]
- for p in paras[:2]:
- parts.append(p[:300])
+ parts.extend(p[:300] for p in paras[:2])
return "
".join([clean_text(p) for p in parts]) if parts else None
-def extract_cards(filepath) -> list[dict[str, str]]:
+def extract_cards(filepath: str) -> list[dict[str, str]]:
"""Extract cards."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
@@ -99,9 +104,9 @@ def extract_cards(filepath) -> list[dict[str, str]]:
content,
re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
- if "Przykład" in header or '"' in header or len(body) < 50:
+ for raw_header, body in sections:
+ header = raw_header.strip()
+ if "Przykład" in header or '"' in header or len(body) < MIN_BODY_LENGTH:
continue
answer = extract_structured_content(body)
@@ -143,8 +148,10 @@ def main() -> None:
for c in unique:
f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
- print(
- f"✅ Approach 2 (Better Extraction): {len(unique)} cards -> {output_file.name}"
+ logger.info(
+ "Approach 2 (Better Extraction): %d cards -> %s",
+ len(unique),
+ output_file.name,
)
diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_generator.py b/python_pkg/praca_magisterska_video/generate_images/anki_generator.py
index a62f6c6..d057477 100755
--- a/python_pkg/praca_magisterska_video/generate_images/anki_generator.py
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_generator.py
@@ -7,31 +7,41 @@ Usage:
Options:
--filter Apply strict filtering (answers > 100 chars)
--extract Use improved extraction algorithm
- --main-only Only generate main exam questions (45 comprehensive cards)
+ --main-only Only generate main exam questions
Combinations:
- python anki_generator.py # Basic extraction, no filter
- python anki_generator.py --filter # Approach 1: Strict filter only
- python anki_generator.py --extract # Approach 2: Better extraction only
- python anki_generator.py --main-only # Approach 3: Main questions only
- python anki_generator.py --filter --extract # Approach 4: Filter + Better extraction
- python anki_generator.py --filter --main-only # Approach 5: Filter + Main only
- python anki_generator.py --extract --main-only # Approach 6: Better extraction + Main only
- python anki_generator.py --filter --extract --main-only # Approach 7: All three
+ python anki_generator.py
+ python anki_generator.py --filter
+ python anki_generator.py --extract
+ python anki_generator.py --main-only
+ python anki_generator.py --filter --extract
+ python anki_generator.py --filter --main-only
+ python anki_generator.py --extract --main-only
+ python anki_generator.py --filter --extract --main-only
"""
from __future__ import annotations
import argparse
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
+
+MIN_PARTS_THRESHOLD = 2
+MIN_BODY_LENGTH = 50
+MIN_PARA_LENGTH = 30
+SHORT_THRESHOLD = 50
+MEDIUM_THRESHOLD = 150
+DEFAULT_MIN_ANSWER_LENGTH = 100
+
# =============================================================================
# SHARED UTILITIES
# =============================================================================
-def clean_text(text) -> str:
+def clean_text(text: str) -> str:
"""Clean and format text for Anki."""
if not text:
return ""
@@ -43,7 +53,7 @@ def clean_text(text) -> str:
return text.strip()
-def get_file_metadata(filepath) -> tuple[str, str, str]:
+def get_file_metadata(filepath: str) -> tuple[str, str, str]:
"""Extract question number and subject from filename."""
filename = Path(filepath).name
match = re.match(r"(\d+)-(.+)\.md", filename)
@@ -58,7 +68,7 @@ def get_file_metadata(filepath) -> tuple[str, str, str]:
return num, subject, content
-def get_main_question(content) -> str | None:
+def get_main_question(content: str) -> str | None:
"""Extract the main exam question."""
q_match = re.search(
r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
@@ -73,7 +83,10 @@ def get_main_question(content) -> str | None:
# =============================================================================
-def apply_strict_filter(cards, min_length=100) -> list[dict[str, str]]:
+def apply_strict_filter(
+ cards: list[dict[str, str]],
+ min_length: int = DEFAULT_MIN_ANSWER_LENGTH,
+) -> list[dict[str, str]]:
"""Filter cards to only include those with answers > min_length characters."""
return [c for c in cards if len(c["back"]) > min_length]
@@ -83,7 +96,7 @@ def apply_strict_filter(cards, min_length=100) -> list[dict[str, str]]:
# =============================================================================
-def extract_structured_content(body) -> str | None:
+def extract_structured_content(body: str) -> str | None:
"""Improved extraction - multiple content types with better formatting."""
parts = []
@@ -101,7 +114,7 @@ def extract_structured_content(body) -> str | None:
parts.append(f"• {term}")
# 3. Key-value patterns
- if len(parts) < 2:
+ if len(parts) < MIN_PARTS_THRESHOLD:
kvs = re.findall(r"\*\*([^*\n]+)\*\*\s*[--:]\s*([^\n*]{10,150})", body)
for k, v in kvs[:4]:
entry = f"{k.strip()}: {v.strip()}"
@@ -116,15 +129,14 @@ def extract_structured_content(body) -> str | None:
if p.strip()
and not p.startswith("```")
and not p.startswith("|")
- and len(p.strip()) > 30
+ and len(p.strip()) > MIN_PARA_LENGTH
]
- for p in paras[:2]:
- parts.append(p[:300])
+ parts.extend(p[:300] for p in paras[:2])
return "
".join([clean_text(p) for p in parts]) if parts else None
-def extract_cards_better(filepath) -> list[dict[str, str]]:
+def extract_cards_better(filepath: str) -> list[dict[str, str]]:
"""Extract cards with improved algorithm."""
num, subject, content = get_file_metadata(filepath)
base_tags = f"egzamin pyt{num} {subject}"
@@ -153,13 +165,13 @@ def extract_cards_better(filepath) -> list[dict[str, str]]:
content,
re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
+ for raw_header, body in sections:
+ header = raw_header.strip()
if (
"Przykład" in header
or '"' in header
or "Mnemonic" in header
- or len(body) < 50
+ or len(body) < MIN_BODY_LENGTH
):
continue
@@ -176,7 +188,7 @@ def extract_cards_better(filepath) -> list[dict[str, str]]:
return cards
-def extract_cards_basic(filepath) -> list[dict[str, str]]:
+def extract_cards_basic(filepath: str) -> list[dict[str, str]]:
"""Basic extraction - simpler algorithm."""
num, subject, content = get_file_metadata(filepath)
base_tags = f"egzamin pyt{num} {subject}"
@@ -212,10 +224,10 @@ def extract_cards_basic(filepath) -> list[dict[str, str]]:
content,
re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
- body = body.strip()
- if len(body) < 50 or "Przykład" in header:
+ for raw_header, raw_body in sections:
+ header = raw_header.strip()
+ body = raw_body.strip()
+ if len(body) < MIN_BODY_LENGTH or "Przykład" in header:
continue
paras = [
@@ -241,7 +253,28 @@ def extract_cards_basic(filepath) -> list[dict[str, str]]:
# =============================================================================
-def extract_main_only(filepath) -> list[dict[str, str]]:
+def _extract_key_point(body: str) -> str | None:
+ """Extract a key point from a section body."""
+ # Try to get a definition or first bullet
+ def_match = re.search(
+ r"Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*", body
+ )
+ if def_match:
+ return def_match.group(1).strip()
+
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)", body)
+ if bullets:
+ term, desc = bullets[0]
+ return f"{term}: {desc.strip()}" if desc.strip() else term
+
+ para_match = re.search(r"\n\n([^#\n\-•|`][^\n]{20,150})", body)
+ if para_match:
+ return para_match.group(1).strip()
+
+ return None
+
+
+def extract_main_only(filepath: str) -> list[dict[str, str]]:
"""Extract only the main exam question with comprehensive answer."""
num, subject, content = get_file_metadata(filepath)
base_tags = f"egzamin pyt{num} {subject} main"
@@ -255,7 +288,9 @@ def extract_main_only(filepath) -> list[dict[str, str]]:
# Get main answer section
answer_match = re.search(
- r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [^�]|\Z)", content, re.DOTALL
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [^�]|\Z)",
+ content,
+ re.DOTALL,
)
if answer_match:
section = answer_match.group(1)
@@ -267,32 +302,16 @@ def extract_main_only(filepath) -> list[dict[str, str]]:
re.MULTILINE | re.DOTALL,
)
- for header, body in headers[:5]:
- header = header.strip()
- if "Przykład" in header or "Mnemonic" in header or '"' in header:
+ for raw_header, body in headers[:5]:
+ header = raw_header.strip()
+ if (
+ "Przykład" in header
+ or "Mnemonic" in header
+ or '"' in header
+ ):
continue
- # Get key point from this section
- key_point = None
-
- # Try to get a definition or first bullet
- def_match = re.search(
- r"Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*", body
- )
- if def_match:
- key_point = def_match.group(1).strip()
-
- if not key_point:
- bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)", body)
- if bullets:
- term, desc = bullets[0]
- key_point = f"{term}: {desc.strip()}" if desc.strip() else term
-
- if not key_point:
- para_match = re.search(r"\n\n([^#\n\-•|`][^\n]{20,150})", body)
- if para_match:
- key_point = para_match.group(1).strip()
-
+ key_point = _extract_key_point(body)
if key_point:
answer_parts.append(f"{header}: {key_point}")
@@ -308,9 +327,58 @@ def extract_main_only(filepath) -> list[dict[str, str]]:
# =============================================================================
-def generate_anki(use_filter=False, use_better_extract=False, main_only=False) -> Path:
+def _collect_cards(
+ odpowiedzi_dir: Path,
+ *,
+ use_better_extract: bool,
+ main_only: bool,
+) -> list[dict[str, str]]:
+ """Collect cards from all files using the specified approach."""
+ all_cards = []
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ if main_only:
+ cards = extract_main_only(md_file)
+ elif use_better_extract:
+ cards = extract_cards_better(md_file)
+ else:
+ cards = extract_cards_basic(md_file)
+ all_cards.extend(cards)
+ return all_cards
+
+
+def _log_statistics(unique: list[dict[str, str]], output_file: Path) -> None:
+ """Log quality statistics for the generated cards."""
+ lengths = [len(c["back"]) for c in unique]
+ short = sum(1 for length in lengths if length < SHORT_THRESHOLD)
+ medium = sum(
+ 1
+ for length in lengths
+ if SHORT_THRESHOLD <= length < MEDIUM_THRESHOLD
+ )
+ good = sum(
+ 1 for length in lengths if length >= MEDIUM_THRESHOLD
+ )
+
+ logger.info("Generated: %s", output_file.name)
+ logger.info(" Cards: %d", len(unique))
+ logger.info(
+ " Quality: %d short / %d medium / %d good",
+ short,
+ medium,
+ good,
+ )
+
+
+def generate_anki(
+ *,
+ use_filter: bool = False,
+ use_better_extract: bool = False,
+ main_only: bool = False,
+) -> Path:
"""Generate Anki deck with specified approaches."""
- odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ odpowiedzi_dir = Path(
+ "/home/kuchy/praca_magisterska/pytania/odpowiedzi"
+ )
# Determine output filename based on options
suffix_parts = []
@@ -322,30 +390,25 @@ def generate_anki(use_filter=False, use_better_extract=False, main_only=False) -
suffix_parts.append("main")
suffix = "_".join(suffix_parts) if suffix_parts else "basic"
- output_file = Path(f"/home/kuchy/praca_magisterska/pytania/anki_{suffix}.txt")
+ output_file = Path(
+ f"/home/kuchy/praca_magisterska/pytania/anki_{suffix}.txt"
+ )
deck_name = f"Egzamin_{suffix.replace('_', '+')}"
- all_cards = []
-
- for md_file in sorted(odpowiedzi_dir.glob("*.md")):
- if main_only:
- # Approach 3: Only main questions
- cards = extract_main_only(md_file)
- elif use_better_extract:
- # Approach 2: Better extraction
- cards = extract_cards_better(md_file)
- else:
- # Basic extraction
- cards = extract_cards_basic(md_file)
-
- all_cards.extend(cards)
+ all_cards = _collect_cards(
+ odpowiedzi_dir,
+ use_better_extract=use_better_extract,
+ main_only=main_only,
+ )
# Approach 1: Apply filtering if requested
if use_filter:
- all_cards = apply_strict_filter(all_cards, min_length=100)
+ all_cards = apply_strict_filter(
+ all_cards, min_length=DEFAULT_MIN_ANSWER_LENGTH
+ )
# Remove duplicates
- seen = set()
+ seen: set[str] = set()
unique = []
for c in all_cards:
key = c["front"][:80]
@@ -355,20 +418,14 @@ def generate_anki(use_filter=False, use_better_extract=False, main_only=False) -
# Write output
with Path(output_file).open("w", encoding="utf-8") as f:
- f.write(f"#separator:Tab\n#html:true\n#notetype:Basic\n#deck:{deck_name}\n\n")
+ f.write(
+ "#separator:Tab\n#html:true\n"
+ f"#notetype:Basic\n#deck:{deck_name}\n\n"
+ )
for c in unique:
f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
- # Statistics
- lengths = [len(c["back"]) for c in unique]
- short = sum(1 for l in lengths if l < 50)
- medium = sum(1 for l in lengths if 50 <= l < 150)
- good = sum(1 for l in lengths if l >= 150)
-
- print(f"✅ Generated: {output_file.name}")
- print(f" Cards: {len(unique)}")
- print(f" Quality: {short} short / {medium} medium / {good} good")
- print()
+ _log_statistics(unique, output_file)
return output_file
@@ -397,9 +454,9 @@ def main() -> None:
if args.all_combinations:
# Generate all 7 combinations
- print("=" * 60)
- print("Generating all 7 combinations...")
- print("=" * 60 + "\n")
+ logger.info("=" * 60)
+ logger.info("Generating all 7 combinations...")
+ logger.info("=" * 60)
combinations = [
(True, False, False), # 1: Filter only
@@ -411,9 +468,22 @@ def main() -> None:
(True, True, True), # 7: All three
]
- for i, (f, e, m) in enumerate(combinations, 1):
- print(f"--- Combination {i} (filter={f}, extract={e}, main={m}) ---")
- generate_anki(use_filter=f, use_better_extract=e, main_only=m)
+ for i, (f_flag, e_flag, m_flag) in enumerate(
+ combinations, 1
+ ):
+ logger.info(
+ "--- Combination %d (filter=%s, extract=%s,"
+ " main=%s) ---",
+ i,
+ f_flag,
+ e_flag,
+ m_flag,
+ )
+ generate_anki(
+ use_filter=f_flag,
+ use_better_extract=e_flag,
+ main_only=m_flag,
+ )
else:
generate_anki(
use_filter=args.filter,
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
index 0e9acdf..14c60a2 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
@@ -12,15 +12,24 @@ All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
from __future__ import annotations
+from dataclasses import dataclass
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING
+
import matplotlib as mpl
mpl.use("Agg")
-from pathlib import Path
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+
+logger = logging.getLogger(__name__)
+
DPI = 300
BG = "white"
LN = "black"
@@ -36,85 +45,160 @@ GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
+@dataclass(frozen=True)
+class BoxStyle:
+ """Optional styling for boxes."""
+
+ fill: str = "white"
+ lw: float = 1.2
+ fontsize: float = FS
+ fontweight: str = "normal"
+ ha: str = "center"
+ va: str = "center"
+ rounded: bool = True
+
+
+@dataclass(frozen=True)
+class ArrowCfg:
+ """Optional config for arrows."""
+
+ lw: float = 1.2
+ style: str = "->"
+ color: str = LN
+ label: str = ""
+ label_offset: float = 0.12
+
+
+@dataclass(frozen=True)
+class DashedArrowCfg:
+ """Optional config for dashed arrows."""
+
+ lw: float = 1.0
+ color: str = LN
+ label: str = ""
+ label_offset: float = 0.12
+
+
def draw_box(
- ax,
- x,
- y,
- w,
- h,
- text,
- fill="white",
- lw=1.2,
- fontsize=FS,
- fontweight="normal",
- ha="center",
- va="center",
- rounded=True,
+ ax: Axes,
+ pos: tuple[float, float],
+ size: tuple[float, float],
+ text: str,
+ style: BoxStyle | None = None,
) -> None:
"""Draw box."""
- if rounded:
+ s = style or BoxStyle()
+ x, y = pos
+ w, h = size
+ if s.rounded:
rect = FancyBboxPatch(
- (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=s.lw,
+ edgecolor=LN,
+ facecolor=s.fill,
)
else:
- rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ h,
+ lw=s.lw,
+ edgecolor=LN,
+ facecolor=s.fill,
+ )
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
- ha=ha,
- va=va,
- fontsize=fontsize,
- fontweight=fontweight,
+ ha=s.ha,
+ va=s.va,
+ fontsize=s.fontsize,
+ fontweight=s.fontweight,
wrap=True,
)
def draw_arrow(
- ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN, label="", label_offset=0.12
+ ax: Axes,
+ start: tuple[float, float],
+ end: tuple[float, float],
+ cfg: ArrowCfg | None = None,
) -> None:
"""Draw arrow."""
+ c = cfg or ArrowCfg()
ax.annotate(
"",
- xy=(x2, y2),
- xytext=(x1, y1),
- arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ xy=end,
+ xytext=start,
+ arrowprops={
+ "arrowstyle": c.style,
+ "color": c.color,
+ "lw": c.lw,
+ },
)
- if label:
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
- ax.text(mx, my, label, ha="center", va="bottom", fontsize=6.5, color=color)
+ if c.label:
+ mx = (start[0] + end[0]) / 2
+ my = (start[1] + end[1]) / 2 + c.label_offset
+ ax.text(
+ mx,
+ my,
+ c.label,
+ ha="center",
+ va="bottom",
+ fontsize=6.5,
+ color=c.color,
+ )
def draw_dashed_arrow(
- ax, x1, y1, x2, y2, lw=1.0, color=LN, label="", label_offset=0.12
+ ax: Axes,
+ start: tuple[float, float],
+ end: tuple[float, float],
+ cfg: DashedArrowCfg | None = None,
) -> None:
"""Draw dashed arrow."""
+ c = cfg or DashedArrowCfg()
ax.annotate(
"",
- xy=(x2, y2),
- xytext=(x1, y1),
+ xy=end,
+ xytext=start,
arrowprops={
"arrowstyle": "->",
- "color": color,
- "lw": lw,
+ "color": c.color,
+ "lw": c.lw,
"linestyle": "dashed",
},
)
- if label:
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
- ax.text(mx, my, label, ha="center", va="bottom", fontsize=6.5, color=color)
+ if c.label:
+ mx = (start[0] + end[0]) / 2
+ my = (start[1] + end[1]) / 2 + c.label_offset
+ ax.text(
+ mx,
+ my,
+ c.label,
+ ha="center",
+ va="bottom",
+ fontsize=6.5,
+ color=c.color,
+ )
-# ─── DIAGRAM 1: See-Think-Act Cycle ──────────────────────────────
+# --- DIAGRAM 1: See-Think-Act Cycle ---
def draw_see_think_act() -> None:
"""Draw see think act."""
- fig, ax = plt.subplots(1, 1, figsize=(7, 4.5), facecolor=BG)
+ fig, ax = plt.subplots(
+ 1, 1, figsize=(7, 4.5), facecolor=BG
+ )
ax.set_xlim(0, 7)
ax.set_ylim(0, 4.5)
ax.axis("off")
ax.set_title(
- "Cykl agenta upostaciowionego: Percepcja → Deliberacja → Akcja",
+ "Cykl agenta upostaciowionego:"
+ " Percepcja \u2192 Deliberacja \u2192 Akcja",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
@@ -135,7 +219,8 @@ def draw_see_think_act() -> None:
ax.text(
3.5,
0.7,
- "ŚRODOWISKO FIZYCZNE\n(przeszkody, obiekty, ludzie)",
+ "\u015aRODOWISKO FIZYCZNE\n"
+ "(przeszkody, obiekty, ludzie)",
ha="center",
va="center",
fontsize=FS,
@@ -167,18 +252,17 @@ def draw_see_think_act() -> None:
bw = 1.4
bh = 0.7
by = 2.2
+ bold_fs8 = BoxStyle(
+ fill=GRAY2, fontsize=8, fontweight="bold"
+ )
# SEE
draw_box(
ax,
- 0.8,
- by,
- bw,
- bh,
+ (0.8, by),
+ (bw, bh),
"SEE\n(Percepcja)",
- fill=GRAY2,
- fontsize=8,
- fontweight="bold",
+ bold_fs8,
)
ax.text(
1.5,
@@ -193,14 +277,12 @@ def draw_see_think_act() -> None:
# THINK
draw_box(
ax,
- 2.8,
- by,
- bw,
- bh,
+ (2.8, by),
+ (bw, bh),
"THINK\n(Deliberacja)",
- fill=GRAY3,
- fontsize=8,
- fontweight="bold",
+ BoxStyle(
+ fill=GRAY3, fontsize=8, fontweight="bold"
+ ),
)
ax.text(
3.5,
@@ -214,7 +296,11 @@ def draw_see_think_act() -> None:
# ACT
draw_box(
- ax, 4.8, by, bw, bh, "ACT\n(Akcja)", fill=GRAY2, fontsize=8, fontweight="bold"
+ ax,
+ (4.8, by),
+ (bw, bh),
+ "ACT\n(Akcja)",
+ bold_fs8,
)
ax.text(
5.5,
@@ -228,17 +314,43 @@ def draw_see_think_act() -> None:
# Arrows between phases
draw_arrow(
- ax, 0.8 + bw, by + bh / 2, 2.8, by + bh / 2, lw=1.5, label="dane sensoryczne"
+ ax,
+ (0.8 + bw, by + bh / 2),
+ (2.8, by + bh / 2),
+ ArrowCfg(lw=1.5, label="dane sensoryczne"),
)
draw_arrow(
- ax, 2.8 + bw, by + bh / 2, 4.8, by + bh / 2, lw=1.5, label="komendy sterujące"
+ ax,
+ (2.8 + bw, by + bh / 2),
+ (4.8, by + bh / 2),
+ ArrowCfg(
+ lw=1.5, label="komendy steruj\u0105ce"
+ ),
)
# Arrows to/from environment
- draw_arrow(ax, 1.5, 1.2, 1.5, by, lw=1.3, label="odczyt", label_offset=0.08)
- draw_arrow(ax, 5.5, by, 5.5, 1.2, lw=1.3, label="działanie", label_offset=0.08)
+ draw_arrow(
+ ax,
+ (1.5, 1.2),
+ (1.5, by),
+ ArrowCfg(
+ lw=1.3,
+ label="odczyt",
+ label_offset=0.08,
+ ),
+ )
+ draw_arrow(
+ ax,
+ (5.5, by),
+ (5.5, 1.2),
+ ArrowCfg(
+ lw=1.3,
+ label="dzia\u0142anie",
+ label_offset=0.08,
+ ),
+ )
- # Feedback loop arrow (from ACT back to environment back to SEE)
+ # Feedback loop arrow
ax.annotate(
"",
xy=(1.5, 1.15),
@@ -254,7 +366,8 @@ def draw_see_think_act() -> None:
ax.text(
3.5,
0.35,
- "← sprzężenie zwrotne (efekt akcji zmienia środowisko) →",
+ "\u2190 sprz\u0119\u017cenie zwrotne"
+ " (efekt akcji zmienia \u015brodowisko) \u2192",
ha="center",
va="center",
fontsize=6,
@@ -262,21 +375,28 @@ def draw_see_think_act() -> None:
)
fig.tight_layout()
- path = str(Path(OUTPUT_DIR) / "agent_see_think_act.png")
- fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ path = str(
+ Path(OUTPUT_DIR) / "agent_see_think_act.png"
+ )
+ fig.savefig(
+ path, dpi=DPI, bbox_inches="tight", facecolor=BG
+ )
plt.close(fig)
- print(f" ✓ {path}")
+ logger.info(" \u2713 %s", path)
-# ─── DIAGRAM 2: 3T Architecture ─────────────────────────────────
+# --- DIAGRAM 2: 3T Architecture ---
def draw_3t_architecture() -> None:
"""Draw 3t architecture."""
- fig, ax = plt.subplots(1, 1, figsize=(7, 5.5), facecolor=BG)
+ fig, ax = plt.subplots(
+ 1, 1, figsize=(7, 5.5), facecolor=BG
+ )
ax.set_xlim(0, 7)
ax.set_ylim(0, 5.5)
ax.axis("off")
ax.set_title(
- "Architektura 3T sterownika robota (3-Layer Architecture)",
+ "Architektura 3T sterownika robota"
+ " (3-Layer Architecture)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
@@ -288,46 +408,52 @@ def draw_3t_architecture() -> None:
"name": "WARSTWA 3: PLANNER\n(Deliberacja)",
"time": "sekundy \u2013 minuty",
"fill": GRAY1,
- "example": 'Cel: "Jed\u017a do kuchni po kubek"\nPlanowanie trasy A \u2192 B \u2192 C',
- "tech": "Planowanie symboliczne\nA*, graf zada\u0144",
+ "example": (
+ 'Cel: "Jed\u017a do kuchni po kubek"\n'
+ "Planowanie trasy A \u2192 B \u2192 C"
+ ),
},
{
"y": 2.6,
"name": "WARSTWA 2: SEQUENCER\n(Wykonawca)",
"time": "100 ms \u2013 sekundy",
"fill": GRAY2,
- "example": "Sekwencja: Jed\u017a do drzwi \u2192\nOtw\u00f3rz \u2192 Jed\u017a do blatu \u2192 Chwy\u0107",
- "tech": "FSM, Behavior Trees\nkoordynacja zachowa\u0144",
+ "example": (
+ "Sekwencja: Jed\u017a do drzwi \u2192\n"
+ "Otw\u00f3rz \u2192 Jed\u017a do blatu"
+ " \u2192 Chwy\u0107"
+ ),
},
{
"y": 1.2,
"name": "WARSTWA 1: CONTROLLER\n(Reaktywny)",
"time": "milisekundy",
"fill": GRAY3,
- "example": "PID: utrzymaj pr\u0119dko\u015b\u0107 0.5 m/s\nUnikaj kolizji (emergency stop)",
- "tech": "PID, regulacja\nbezpo\u015brednie I/O",
+ "example": (
+ "PID: utrzymaj pr\u0119dko\u015b\u0107"
+ " 0.5 m/s\n"
+ "Unikaj kolizji (emergency stop)"
+ ),
},
]
bw = 4.0
bh = 0.85
- for _i, layer in enumerate(layers):
+ for layer in layers:
y = layer["y"]
- # Main layer box
draw_box(
ax,
- 0.3,
- y,
- bw,
- bh,
+ (0.3, y),
+ (bw, bh),
layer["name"],
- fill=layer["fill"],
- fontsize=8,
- fontweight="bold",
+ BoxStyle(
+ fill=layer["fill"],
+ fontsize=8,
+ fontweight="bold",
+ ),
)
- # Time label (left)
ax.text(
0.15,
y + bh / 2,
@@ -345,33 +471,38 @@ def draw_3t_architecture() -> None:
},
)
- # Example (right side)
- draw_box(ax, 4.5, y, 2.3, bh, layer["example"], fill="white", fontsize=6.5)
+ draw_box(
+ ax,
+ (4.5, y),
+ (2.3, bh),
+ layer["example"],
+ BoxStyle(fontsize=6.5),
+ )
- # Technology used
- # ax.text(4.5 + 1.15, y - 0.12, layer["tech"], ha='center', va='top',
- # fontsize=5.5, fontstyle='italic', color='#555555')
-
- # Arrows between layers (downward = commands, upward = status)
+ # Arrows between layers
for i in range(len(layers) - 1):
y_top = layers[i]["y"]
y_bot = layers[i + 1]["y"] + 0.85
- # Command arrow (down)
- draw_arrow(
- ax, 1.8, y_top, 1.8, y_bot, lw=1.3, label="polecenia ↓", label_offset=0.02
- )
- # Status arrow (up)
draw_arrow(
ax,
- 2.8,
- y_bot,
- 2.8,
- y_top,
- lw=1.0,
- style="->",
- color="#666666",
- label="↑ status",
- label_offset=0.02,
+ (1.8, y_top),
+ (1.8, y_bot),
+ ArrowCfg(
+ lw=1.3,
+ label="polecenia \u2193",
+ label_offset=0.02,
+ ),
+ )
+ draw_arrow(
+ ax,
+ (2.8, y_bot),
+ (2.8, y_top),
+ ArrowCfg(
+ lw=1.0,
+ color="#666666",
+ label="\u2191 status",
+ label_offset=0.02,
+ ),
)
# Environment at bottom
@@ -389,22 +520,27 @@ def draw_3t_architecture() -> None:
ax.text(
0.3 + bw / 2,
0.6,
- "SPRZĘT: silniki, czujniki, efektory",
+ "SPRZ\u0118T: silniki, czujniki, efektory",
ha="center",
va="center",
fontsize=7,
fontstyle="italic",
)
- # Arrow from controller to hardware
- draw_arrow(ax, 2.3, 1.2, 2.3, 0.9, lw=1.3)
+ draw_arrow(
+ ax, (2.3, 1.2), (2.3, 0.9), ArrowCfg(lw=1.3)
+ )
# Abstraction label on the right
ax.annotate(
"",
xy=(6.9, 4.8),
xytext=(6.9, 0.5),
- arrowprops={"arrowstyle": "<->", "color": "#888888", "lw": 1.0},
+ arrowprops={
+ "arrowstyle": "<->",
+ "color": "#888888",
+ "lw": 1.0,
+ },
)
ax.text(
6.95,
@@ -418,33 +554,43 @@ def draw_3t_architecture() -> None:
)
fig.tight_layout()
- path = str(Path(OUTPUT_DIR) / "agent_3t_architecture.png")
- fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ path = str(
+ Path(OUTPUT_DIR) / "agent_3t_architecture.png"
+ )
+ fig.savefig(
+ path, dpi=DPI, bbox_inches="tight", facecolor=BG
+ )
plt.close(fig)
- print(f" ✓ {path}")
+ logger.info(" \u2713 %s", path)
-# ─── DIAGRAM 3: Behavior Tree Example ────────────────────────────
+# --- DIAGRAM 3: Behavior Tree Example ---
def draw_behavior_tree() -> None:
"""Draw behavior tree."""
- fig, ax = plt.subplots(1, 1, figsize=(7.5, 4.5), facecolor=BG)
+ fig, ax = plt.subplots(
+ 1, 1, figsize=(7.5, 4.5), facecolor=BG
+ )
ax.set_xlim(0, 7.5)
ax.set_ylim(0, 4.5)
ax.axis("off")
ax.set_title(
- "Behavior Tree: robot przenoszący obiekt (pick-and-place)",
+ "Behavior Tree: robot przenosz\u0105cy"
+ " obiekt (pick-and-place)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
- # Node positions: (x, y, text, shape_type)
- # shape_type: 'seq' = sequence (→), 'sel' = selector (?), 'act' = action, 'cond' = condition
-
- def draw_bt_node(ax, x, y, text, ntype="act", w=1.0, h=0.45) -> tuple[float, float]:
+ def draw_bt_node(
+ pos: tuple[float, float],
+ text: str,
+ ntype: str = "act",
+ size: tuple[float, float] = (1.0, 0.45),
+ ) -> tuple[float, float]:
"""Draw a behavior tree node."""
+ x, y = pos
+ w, h = size
if ntype == "seq":
- # Sequence = box with →
rect = FancyBboxPatch(
(x - w / 2, y - h / 2),
w,
@@ -458,14 +604,13 @@ def draw_behavior_tree() -> None:
ax.text(
x,
y,
- f"→ {text}",
+ f"\u2192 {text}",
ha="center",
va="center",
fontsize=7,
fontweight="bold",
)
elif ntype == "sel":
- # Selector = box with ?
rect = FancyBboxPatch(
(x - w / 2, y - h / 2),
w,
@@ -486,7 +631,6 @@ def draw_behavior_tree() -> None:
fontweight="bold",
)
elif ntype == "cond":
- # Condition = diamond-ish / oval with dashed border
rect = FancyBboxPatch(
(x - w / 2, y - h / 2),
w,
@@ -499,7 +643,13 @@ def draw_behavior_tree() -> None:
)
ax.add_patch(rect)
ax.text(
- x, y, text, ha="center", va="center", fontsize=6.5, fontstyle="italic"
+ x,
+ y,
+ text,
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ fontstyle="italic",
)
else: # action
rect = FancyBboxPatch(
@@ -512,53 +662,121 @@ def draw_behavior_tree() -> None:
facecolor=GRAY1,
)
ax.add_patch(rect)
- ax.text(x, y, text, ha="center", va="center", fontsize=6.5)
+ ax.text(
+ x,
+ y,
+ text,
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ )
return x, y
- # Root: Sequence "Przenieś obiekt"
- root = draw_bt_node(ax, 3.75, 3.8, "Przenieś obiekt", "seq", w=1.6)
+ # Root: Sequence "Przenies obiekt"
+ root = draw_bt_node(
+ (3.75, 3.8), "Przenie\u015b obiekt", "seq",
+ (1.6, 0.45),
+ )
# Level 2 children
- find = draw_bt_node(ax, 1.2, 2.8, "Znajdź obiekt", "sel", w=1.3)
- nav = draw_bt_node(ax, 3.75, 2.8, "Jedź do obiektu", "act", w=1.3)
- pick = draw_bt_node(ax, 6.3, 2.8, "Chwyć i dostarcz", "seq", w=1.4)
+ find = draw_bt_node(
+ (1.2, 2.8), "Znajd\u017a obiekt", "sel",
+ (1.3, 0.45),
+ )
+ nav = draw_bt_node(
+ (3.75, 2.8), "Jed\u017a do obiektu", "act",
+ (1.3, 0.45),
+ )
+ pick = draw_bt_node(
+ (6.3, 2.8), "Chwy\u0107 i dostarcz", "seq",
+ (1.4, 0.45),
+ )
# Arrows from root
- draw_arrow(ax, root[0], root[1] - 0.225, find[0], find[1] + 0.225, lw=1.0)
- draw_arrow(ax, root[0], root[1] - 0.225, nav[0], nav[1] + 0.225, lw=1.0)
- draw_arrow(ax, root[0], root[1] - 0.225, pick[0], pick[1] + 0.225, lw=1.0)
+ arrow_thin = ArrowCfg(lw=1.0)
+ for child in (find, nav, pick):
+ draw_arrow(
+ ax,
+ (root[0], root[1] - 0.225),
+ (child[0], child[1] + 0.225),
+ arrow_thin,
+ )
- # Level 3: children of "Znajdź obiekt" (selector)
- vis = draw_bt_node(ax, 0.55, 1.7, "Widzę\nobiekt?", "cond", w=0.85, h=0.5)
- scan = draw_bt_node(ax, 1.85, 1.7, "Skanuj\notoczenie", "act", w=0.85, h=0.5)
- draw_arrow(ax, find[0], find[1] - 0.225, vis[0], vis[1] + 0.25, lw=0.8)
- draw_arrow(ax, find[0], find[1] - 0.225, scan[0], scan[1] + 0.25, lw=0.8)
+ # Level 3: children of "Znajdz obiekt"
+ arrow_08 = ArrowCfg(lw=0.8)
+ vis = draw_bt_node(
+ (0.55, 1.7), "Widz\u0119\nobiekt?", "cond",
+ (0.85, 0.5),
+ )
+ scan = draw_bt_node(
+ (1.85, 1.7), "Skanuj\notoczenie", "act",
+ (0.85, 0.5),
+ )
+ for child in (vis, scan):
+ draw_arrow(
+ ax,
+ (find[0], find[1] - 0.225),
+ (child[0], child[1] + 0.25),
+ arrow_08,
+ )
- # Level 3: children of "Chwyć i dostarcz" (sequence)
- grasp = draw_bt_node(ax, 5.4, 1.7, "Chwyć\nobject", "act", w=0.85, h=0.5)
- deliver = draw_bt_node(ax, 6.5, 1.7, "Jedź do\ncelu", "act", w=0.85, h=0.5)
- release = draw_bt_node(ax, 7.2, 1.7, "Puść", "act", w=0.55, h=0.5)
- draw_arrow(ax, pick[0], pick[1] - 0.225, grasp[0], grasp[1] + 0.25, lw=0.8)
- draw_arrow(ax, pick[0], pick[1] - 0.225, deliver[0], deliver[1] + 0.25, lw=0.8)
- draw_arrow(ax, pick[0], pick[1] - 0.225, release[0], release[1] + 0.25, lw=0.8)
+ # Level 3: children of "Chwyt i dostarcz"
+ pick_children = [
+ draw_bt_node(
+ (5.4, 1.7), "Chwy\u0107\nobject", "act",
+ (0.85, 0.5),
+ ),
+ draw_bt_node(
+ (6.5, 1.7), "Jed\u017a do\ncelu", "act",
+ (0.85, 0.5),
+ ),
+ draw_bt_node(
+ (7.2, 1.7), "Pu\u015b\u0107", "act",
+ (0.55, 0.5),
+ ),
+ ]
+ for child in pick_children:
+ draw_arrow(
+ ax,
+ (pick[0], pick[1] - 0.225),
+ (child[0], child[1] + 0.25),
+ arrow_08,
+ )
# Legend
leg_y = 0.5
- draw_bt_node(ax, 0.8, leg_y, "→ Sequence", "seq", w=1.1, h=0.35)
- draw_bt_node(ax, 2.3, leg_y, "? Selector", "sel", w=1.0, h=0.35)
- draw_bt_node(ax, 3.6, leg_y, "Akcja", "act", w=0.8, h=0.35)
- draw_bt_node(ax, 4.8, leg_y, "Warunek", "cond", w=0.8, h=0.35)
+ draw_bt_node(
+ (0.8, leg_y), "\u2192 Sequence", "seq",
+ (1.1, 0.35),
+ )
+ draw_bt_node(
+ (2.3, leg_y), "? Selector", "sel",
+ (1.0, 0.35),
+ )
+ draw_bt_node(
+ (3.6, leg_y), "Akcja", "act", (0.8, 0.35)
+ )
+ draw_bt_node(
+ (4.8, leg_y), "Warunek", "cond", (0.8, 0.35)
+ )
ax.text(
- 0.3, leg_y, "Legenda:", ha="left", va="center", fontsize=6.5, fontweight="bold"
+ 0.3,
+ leg_y,
+ "Legenda:",
+ ha="left",
+ va="center",
+ fontsize=6.5,
+ fontweight="bold",
)
# Execution note
ax.text(
3.75,
0.05,
- "Wykonanie: od lewej do prawej. Sequence (→) = wszystkie po kolei. "
- "Selector (?) = pierwszy sukces.",
+ "Wykonanie: od lewej do prawej."
+ " Sequence (\u2192) = wszystkie po kolei."
+ " Selector (?) = pierwszy sukces.",
ha="center",
va="center",
fontsize=6,
@@ -567,21 +785,28 @@ def draw_behavior_tree() -> None:
)
fig.tight_layout()
- path = str(Path(OUTPUT_DIR) / "agent_behavior_tree.png")
- fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ path = str(
+ Path(OUTPUT_DIR) / "agent_behavior_tree.png"
+ )
+ fig.savefig(
+ path, dpi=DPI, bbox_inches="tight", facecolor=BG
+ )
plt.close(fig)
- print(f" ✓ {path}")
+ logger.info(" \u2713 %s", path)
-# ─── DIAGRAM 4: BDI Model ────────────────────────────────────────
+# --- DIAGRAM 4: BDI Model ---
def draw_bdi_model() -> None:
"""Draw bdi model."""
- fig, ax = plt.subplots(1, 1, figsize=(7, 4), facecolor=BG)
+ fig, ax = plt.subplots(
+ 1, 1, figsize=(7, 4), facecolor=BG
+ )
ax.set_xlim(0, 7)
ax.set_ylim(0, 4)
ax.axis("off")
ax.set_title(
- "Model BDI agenta (Beliefs-Desires-Intentions)",
+ "Model BDI agenta"
+ " (Beliefs-Desires-Intentions)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
@@ -589,9 +814,12 @@ def draw_bdi_model() -> None:
bw = 1.6
bh = 1.4
+ bold8 = BoxStyle(
+ fill=GRAY1, fontsize=8, fontweight="bold"
+ )
# BELIEFS box
- draw_box(ax, 0.3, 1.3, bw, bh, "", fill=GRAY1, fontsize=8, fontweight="bold")
+ draw_box(ax, (0.3, 1.3), (bw, bh), "", bold8)
ax.text(
0.3 + bw / 2,
1.3 + bh - 0.15,
@@ -604,14 +832,26 @@ def draw_bdi_model() -> None:
ax.text(
0.3 + bw / 2,
1.3 + bh / 2 - 0.1,
- "(wiedza o \u015bwiecie)\n\n\u2022 mapa pokoju\n\u2022 pozycja robota\n\u2022 drzwi zamkni\u0119te\n\u2022 bateria: 45%",
+ "(wiedza o \u015bwiecie)\n\n"
+ "\u2022 mapa pokoju\n"
+ "\u2022 pozycja robota\n"
+ "\u2022 drzwi zamkni\u0119te\n"
+ "\u2022 bateria: 45%",
ha="center",
va="center",
fontsize=6.5,
)
# DESIRES box
- draw_box(ax, 2.7, 1.3, bw, bh, "", fill=GRAY2, fontsize=8, fontweight="bold")
+ draw_box(
+ ax,
+ (2.7, 1.3),
+ (bw, bh),
+ "",
+ BoxStyle(
+ fill=GRAY2, fontsize=8, fontweight="bold"
+ ),
+ )
ax.text(
2.7 + bw / 2,
1.3 + bh - 0.15,
@@ -624,14 +864,26 @@ def draw_bdi_model() -> None:
ax.text(
2.7 + bw / 2,
1.3 + bh / 2 - 0.1,
- "(cele agenta)\n\n• dostarczyć paczkę\n do pokoju 5\n• naładować baterię\n• unikać kolizji",
+ "(cele agenta)\n\n"
+ "\u2022 dostarczy\u0107 paczk\u0119\n"
+ " do pokoju 5\n"
+ "\u2022 na\u0142adowa\u0107 bateri\u0119\n"
+ "\u2022 unika\u0107 kolizji",
ha="center",
va="center",
fontsize=6.5,
)
# INTENTIONS box
- draw_box(ax, 5.1, 1.3, bw, bh, "", fill=GRAY3, fontsize=8, fontweight="bold")
+ draw_box(
+ ax,
+ (5.1, 1.3),
+ (bw, bh),
+ "",
+ BoxStyle(
+ fill=GRAY3, fontsize=8, fontweight="bold"
+ ),
+ )
ax.text(
5.1 + bw / 2,
1.3 + bh - 0.15,
@@ -644,7 +896,11 @@ def draw_bdi_model() -> None:
ax.text(
5.1 + bw / 2,
1.3 + bh / 2 - 0.1,
- "(aktualny plan)\n\n→ jedź do drzwi\n bocznych\n→ otwórz drzwi\n→ wjedź do pokoju 5",
+ "(aktualny plan)\n\n"
+ "\u2192 jed\u017a do drzwi\n"
+ " bocznych\n"
+ "\u2192 otw\u00f3rz drzwi\n"
+ "\u2192 wjed\u017a do pokoju 5",
ha="center",
va="center",
fontsize=6.5,
@@ -653,26 +909,26 @@ def draw_bdi_model() -> None:
# Arrows
draw_arrow(
ax,
- 0.3 + bw,
- 1.3 + bh / 2 + 0.15,
- 2.7,
- 1.3 + bh / 2 + 0.15,
- lw=1.3,
- label="informuje",
- label_offset=0.08,
+ (0.3 + bw, 1.3 + bh / 2 + 0.15),
+ (2.7, 1.3 + bh / 2 + 0.15),
+ ArrowCfg(
+ lw=1.3,
+ label="informuje",
+ label_offset=0.08,
+ ),
)
draw_arrow(
ax,
- 2.7 + bw,
- 1.3 + bh / 2 + 0.15,
- 5.1,
- 1.3 + bh / 2 + 0.15,
- lw=1.3,
- label="filtruje → wybiera",
- label_offset=0.08,
+ (2.7 + bw, 1.3 + bh / 2 + 0.15),
+ (5.1, 1.3 + bh / 2 + 0.15),
+ ArrowCfg(
+ lw=1.3,
+ label="filtruje \u2192 wybiera",
+ label_offset=0.08,
+ ),
)
- # Feedback from intentions back to beliefs (update)
+ # Feedback: intentions back to beliefs
ax.annotate(
"",
xy=(0.3 + bw / 2, 1.3),
@@ -699,18 +955,18 @@ def draw_bdi_model() -> None:
# Sensor input arrow
draw_arrow(
ax,
- 0.3 + bw / 2,
- 3.5,
- 0.3 + bw / 2,
- 1.3 + bh,
- lw=1.3,
- label="percepcja (sensory)",
- label_offset=0.05,
+ (0.3 + bw / 2, 3.5),
+ (0.3 + bw / 2, 1.3 + bh),
+ ArrowCfg(
+ lw=1.3,
+ label="percepcja (sensory)",
+ label_offset=0.05,
+ ),
)
ax.text(
0.3 + bw / 2,
3.55,
- "ŚRODOWISKO",
+ "\u015aRODOWISKO",
ha="center",
va="bottom",
fontsize=7,
@@ -726,13 +982,13 @@ def draw_bdi_model() -> None:
# Action output arrow
draw_arrow(
ax,
- 5.1 + bw / 2,
- 1.3 + bh,
- 5.1 + bw / 2,
- 3.5,
- lw=1.3,
- label="akcja (efektory)",
- label_offset=0.05,
+ (5.1 + bw / 2, 1.3 + bh),
+ (5.1 + bw / 2, 3.5),
+ ArrowCfg(
+ lw=1.3,
+ label="akcja (efektory)",
+ label_offset=0.05,
+ ),
)
ax.text(
5.1 + bw / 2,
@@ -752,16 +1008,18 @@ def draw_bdi_model() -> None:
fig.tight_layout()
path = str(Path(OUTPUT_DIR) / "agent_bdi_model.png")
- fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ fig.savefig(
+ path, dpi=DPI, bbox_inches="tight", facecolor=BG
+ )
plt.close(fig)
- print(f" ✓ {path}")
+ logger.info(" \u2713 %s", path)
-# ─── MAIN ────────────────────────────────────────────────────────
+# --- MAIN ---
if __name__ == "__main__":
- print("Generating PYTANIE 15 diagrams...")
+ logger.info("Generating PYTANIE 15 diagrams...")
draw_see_think_act()
draw_3t_architecture()
draw_behavior_tree()
draw_bdi_model()
- print("Done! All diagrams saved to", OUTPUT_DIR)
+ logger.info("Done! All diagrams saved to %s", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki.py
index 3fcd66b..1cfd7c7 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_anki.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki.py
@@ -6,18 +6,27 @@ Creates a tab-separated file compatible with Anki import.
from __future__ import annotations
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
-def extract_question_and_answer(filepath) -> list[dict[str, str]]:
- """Extract main question and key answer points from a markdown file."""
+MIN_BODY_LENGTH = 50
+MIN_DEFINITION_LENGTH = 20
+MAX_DEFINITION_LENGTH = 200
+MIN_BULLET_COUNT = 5
+MIN_SUBSECTION_LENGTH = 5
+MIN_FORMULA_LENGTH = 20
+
+
+def _get_metadata(
+ filepath: str,
+) -> tuple[str, str, str, str, str]:
+ """Extract metadata from file."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
- cards = []
-
- # Extract file number for tagging
filename = Path(filepath).name
match = re.match(r"(\d+)-(.+)\.md", filename)
if match:
@@ -27,13 +36,13 @@ def extract_question_and_answer(filepath) -> list[dict[str, str]]:
num = "00"
topic = "unknown"
- # Extract main title (usually contains the question)
title_match = re.search(r"^# (.+)$", content, re.MULTILINE)
title = title_match.group(1) if title_match else "Unknown"
- # Extract the main question from ## Pytanie section
question_match = re.search(
- r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*',
+ content,
+ re.DOTALL,
)
if question_match:
main_question = question_match.group(1).strip()
@@ -41,124 +50,207 @@ def extract_question_and_answer(filepath) -> list[dict[str, str]]:
else:
main_question = title
- # Extract subject/przedmiot
- subject_match = re.search(r"Przedmiot:\s*(\w+)", content)
- subject = subject_match.group(1) if subject_match else "Ogólne"
+ return num, topic, title, main_question, content
- # Create main question card - extract key sections for answer
- answer_parts = []
- # Look for main answer section
+def _extract_main_card(
+ content: str,
+ main_question: str,
+ subject: str,
+ num: str,
+ topic: str,
+) -> list[dict[str, str]]:
+ """Extract the main question card."""
+ answer_parts: list[str] = []
+
main_answer = re.search(
- r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\n---\s*\n## |\Z)",
+ r"## 📚 Odpowiedź główna\s*\n(.+?)"
+ r"(?=\n## |\n---\s*\n## |\Z)",
content,
re.DOTALL,
)
if main_answer:
answer_text = main_answer.group(1)
- # Extract key points, definitions, headers
headers = re.findall(r"### (.+)", answer_text)
- for h in headers[:5]: # Limit to first 5 headers
- answer_parts.append(f"• {h}")
+ answer_parts.extend(f"• {h}" for h in headers[:5])
- # Also extract key definitions if present
- definitions = re.findall(r"\*\*([^*]+)\*\*\s*[--:]\s*([^*\n]+)", content)
+ definitions = re.findall(
+ r"\*\*([^*]+)\*\*\s*[--:]\s*([^*\n]+)", content
+ )
for term, definition in definitions[:3]:
- if len(definition) > 20 and len(definition) < 200:
- answer_parts.append(f"• {term}: {definition.strip()}")
+ if (
+ len(definition) > MIN_DEFINITION_LENGTH
+ and len(definition) < MAX_DEFINITION_LENGTH
+ ):
+ answer_parts.append(
+ f"• {term}: {definition.strip()}"
+ )
- # If we found answer parts, create main card
- if answer_parts:
- answer_html = "
".join(answer_parts[:8]) # Limit answer length
- cards.append(
- {
- "question": main_question,
- "answer": answer_html,
- "tags": f"egzamin_magisterski pytanie_{num} {subject} {topic}",
- }
+ if not answer_parts:
+ return []
+
+ answer_html = "
".join(answer_parts[:8])
+ return [
+ {
+ "question": main_question,
+ "answer": answer_html,
+ "tags": (
+ f"egzamin_magisterski pytanie_{num}"
+ f" {subject} {topic}"
+ ),
+ }
+ ]
+
+
+def _extract_subsection_answer(body_clean: str) -> str | None:
+ """Extract answer text from a subsection body."""
+ bullets = re.findall(
+ r"[-•]\s*\*\*(.+?)\*\*[:\s]*([^\n]+)?", body_clean
+ )
+ if bullets:
+ return "
".join(
+ f"• {b[0]}: {b[1].strip()}" if b[1] else f"• {b[0]}"
+ for b in bullets[:MIN_BULLET_COUNT]
)
- # Extract sub-questions and key concepts as additional cards
- # Look for ### headers with explanations
+ paragraphs = [
+ p.strip()
+ for p in body_clean.split("\n\n")
+ if p.strip()
+ and not p.startswith("```")
+ and not p.startswith("|")
+ ]
+ if paragraphs:
+ first_para = paragraphs[0]
+ first_para = re.sub(r"\*\*(.+?)\*\*", r"\1", first_para)
+ first_para = re.sub(r"\*(.+?)\*", r"\1", first_para)
+ return first_para[:400]
+
+ return None
+
+
+def _extract_sub_cards(
+ content: str,
+ title: str,
+ subject: str,
+ num: str,
+ topic: str,
+) -> list[dict[str, str]]:
+ """Extract sub-concept cards."""
+ cards: list[dict[str, str]] = []
subsections = re.findall(
- r"### (\d+\.\s+)?(.+?)\n\n(.+?)(?=\n### |\n## |\n---|\Z)", content, re.DOTALL
+ r"### (\d+\.\s+)?(.+?)\n\n(.+?)"
+ r"(?=\n### |\n## |\n---|\Z)",
+ content,
+ re.DOTALL,
)
for _, header, body in subsections:
- if len(header) < 5 or header.startswith("Przykład"):
- continue
-
- # Extract first substantive paragraph or key points
- body_clean = body.strip()
-
- # Skip very short or code-only sections
- if len(body_clean) < 50:
- continue
-
- # Extract bullet points or first paragraph
- bullets = re.findall(r"[-•]\s*\*\*(.+?)\*\*[:\s]*([^\n]+)?", body_clean)
- if bullets:
- answer_text = "
".join(
- [
- f"• {b[0]}: {b[1].strip()}" if b[1] else f"• {b[0]}"
- for b in bullets[:5]
- ]
- )
- else:
- # Get first meaningful paragraph
- paragraphs = [
- p.strip()
- for p in body_clean.split("\n\n")
- if p.strip() and not p.startswith("```") and not p.startswith("|")
- ]
- if paragraphs:
- first_para = paragraphs[0]
- # Clean markdown
- first_para = re.sub(r"\*\*(.+?)\*\*", r"\1", first_para)
- first_para = re.sub(r"\*(.+?)\*", r"\1", first_para)
- answer_text = first_para[:400]
- else:
- continue
-
- # Create sub-concept card
- sub_question = f"Co to jest {header}?" if not header.endswith("?") else header
if (
- "Charakterystyka" in header
- or "Definicja" in header
- or "Właściwości" in header
+ len(header) < MIN_SUBSECTION_LENGTH
+ or header.startswith("Przykład")
):
- # These are answer-type headers, reframe
- parent_topic = title.replace("Pytanie", "").strip(": 0123456789")
- sub_question = f"{header} - {parent_topic}"
+ continue
+
+ body_clean = body.strip()
+ if len(body_clean) < MIN_BODY_LENGTH:
+ continue
+
+ answer_text = _extract_subsection_answer(body_clean)
+ if not answer_text:
+ continue
+
+ sub_question = (
+ f"Co to jest {header}?"
+ if not header.endswith("?")
+ else header
+ )
+
+ if any(
+ kw in header
+ for kw in ("Charakterystyka", "Definicja", "Właściwości")
+ ):
+ parent = title.replace("Pytanie", "").strip(
+ ": 0123456789"
+ )
+ sub_question = f"{header} - {parent}"
cards.append(
{
"question": sub_question,
"answer": answer_text,
- "tags": f"egzamin_magisterski pytanie_{num} {subject} {topic} szczegoly",
+ "tags": (
+ f"egzamin_magisterski pytanie_{num}"
+ f" {subject} {topic} szczegoly"
+ ),
}
)
- # Extract key formulas/definitions as separate cards
+ return cards
+
+
+def _extract_formula_cards(
+ content: str,
+ subject: str,
+ num: str,
+) -> list[dict[str, str]]:
+ """Extract formula/definition cards."""
+ cards: list[dict[str, str]] = []
formulas = re.findall(
- r"\*\*([A-Za-z\s]+(?:formuła|wzór|twierdzenie|definicja|lemat))\*\*[:\s]*\n?(.+?)(?=\n\n|\n\*\*|\Z)",
+ r"\*\*([A-Za-z\s]+"
+ r"(?:formuła|wzór|twierdzenie|definicja|lemat))"
+ r"\*\*[:\s]*\n?(.+?)(?=\n\n|\n\*\*|\Z)",
content,
re.IGNORECASE | re.DOTALL,
)
for formula_name, formula_content in formulas:
- if len(formula_content) > 20:
+ if len(formula_content) > MIN_FORMULA_LENGTH:
cards.append(
{
"question": f"Podaj {formula_name.strip()}",
"answer": formula_content.strip()[:300],
- "tags": f"egzamin_magisterski pytanie_{num} {subject} formuly",
+ "tags": (
+ f"egzamin_magisterski pytanie_{num}"
+ f" {subject} formuly"
+ ),
}
)
return cards
-def clean_for_anki(text) -> str:
+def extract_question_and_answer(
+ filepath: str,
+) -> list[dict[str, str]]:
+ """Extract main question and key answer points from a markdown file."""
+ num, topic, title, main_question, content = _get_metadata(
+ filepath
+ )
+
+ subject_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = (
+ subject_match.group(1) if subject_match else "Ogólne"
+ )
+
+ cards: list[dict[str, str]] = []
+ cards.extend(
+ _extract_main_card(
+ content, main_question, subject, num, topic
+ )
+ )
+ cards.extend(
+ _extract_sub_cards(
+ content, title, subject, num, topic
+ )
+ )
+ cards.extend(
+ _extract_formula_cards(content, subject, num)
+ )
+
+ return cards
+
+
+def clean_for_anki(text: str) -> str:
"""Clean text for Anki import - escape special characters."""
# Replace tabs with spaces
text = text.replace("\t", " ")
@@ -187,13 +279,13 @@ def main() -> None:
# Process each file
for md_file in sorted(odpowiedzi_dir.glob("*.md")):
- print(f"Processing: {md_file.name}")
+ logger.info("Processing: %s", md_file.name)
try:
cards = extract_question_and_answer(md_file)
all_cards.extend(cards)
- print(f" -> Extracted {len(cards)} cards")
- except Exception as e:
- print(f" -> Error: {e}")
+ logger.info(" -> Extracted %d cards", len(cards))
+ except (ValueError, OSError) as e:
+ logger.info(" -> Error: %s", e)
# Write Anki file with headers
with Path(output_file).open("w", encoding="utf-8") as f:
@@ -211,13 +303,13 @@ def main() -> None:
tags = card["tags"]
f.write(f"{front}\t{back}\t{tags}\n")
- print(f"\n✅ Created {len(all_cards)} flashcards")
- print(f"📁 Output: {output_file}")
- print("\nTo import into Anki:")
- print("1. Open Anki → File → Import")
- print("2. Select the .txt file")
- print("3. Verify 'Allow HTML' is checked")
- print("4. Click Import")
+ logger.info("Created %d flashcards", len(all_cards))
+ logger.info("Output: %s", output_file)
+ logger.info("To import into Anki:")
+ logger.info("1. Open Anki -> File -> Import")
+ logger.info("2. Select the .txt file")
+ logger.info("3. Verify 'Allow HTML' is checked")
+ logger.info("4. Click Import")
if __name__ == "__main__":
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
index 7435559..9d36391 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
@@ -6,11 +6,22 @@ Creates tab-separated file for Anki import with proper HTML formatting.
from __future__ import annotations
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
-def clean_text(text) -> str:
+MIN_HEADER_LENGTH = 3
+MIN_MATCH_LENGTH = 10
+MIN_BODY_LENGTH = 50
+MIN_QA_LENGTH = 30
+MAX_CONTENT_LENGTH = 300
+MAX_ANSWER_LENGTH = 400
+MAX_COMPARISON_ITEMS = 6
+
+
+def clean_text(text: str) -> str:
"""Clean and format text for Anki."""
if not text:
return ""
@@ -28,7 +39,7 @@ def clean_text(text) -> str:
return text.strip()
-def format_list(items, numbered=False) -> str:
+def format_list(items: list[str], *, numbered: bool = False) -> str:
"""Format a list of items as HTML."""
if not items:
return ""
@@ -43,119 +54,148 @@ def format_list(items, numbered=False) -> str:
return html
-def extract_from_file(filepath) -> list[dict[str, str]]:
- """Extract flashcard data from a markdown file."""
+def _get_file_metadata(
+ filepath: str,
+) -> tuple[str, str, str]:
+ """Extract metadata from file."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
- cards = []
-
- # Get file metadata
filename = Path(filepath).name
match = re.match(r"(\d+)-(.+)\.md", filename)
num = match.group(1) if match else "00"
- match.group(2).replace("-", "_") if match else "unknown"
- # Extract subject
subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
subject = subj_match.group(1) if subj_match else "Ogólne"
- # Base tags
- base_tags = f"egzamin_magisterski pyt{num} {subject}"
+ return num, subject, content
- # =====================================================
- # CARD TYPE 1: Main Exam Question
- # =====================================================
+
+def _extract_main_question_card(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Extract the main exam question card."""
q_match = re.search(
- r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*',
+ content,
+ re.DOTALL,
)
- if q_match:
- main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
+ if not q_match:
+ return []
- # Extract key topics from main answer
- answer_match = re.search(
- r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [�🎯]|\n---\s*\n## |\Z)",
- content,
- re.DOTALL,
+ main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)"
+ r"(?=\n## [📚🎯]|\n---\s*\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if not answer_match:
+ return []
+
+ answer_section = answer_match.group(1)
+ headers = re.findall(
+ r"^### (?:\d+\.\s*)?(.+)$",
+ answer_section,
+ re.MULTILINE,
+ )
+ headers = [
+ h.strip()
+ for h in headers
+ if len(h.strip()) > MIN_HEADER_LENGTH
+ ][:6]
+
+ if not headers:
+ return []
+
+ answer_html = (
+ "Kluczowe zagadnienia:" + format_list(headers)
+ )
+ return [
+ {
+ "front": clean_text(main_q),
+ "back": answer_html,
+ "tags": f"{base_tags} pytanie_glowne",
+ }
+ ]
+
+
+def _make_question_text(header: str) -> str:
+ """Generate a question from a section header."""
+ if "Definicja" in header or "Co to" in header:
+ return (
+ f"Co to jest:"
+ f" {header.replace('Definicja', '').strip()}?"
)
- if answer_match:
- answer_section = answer_match.group(1)
- # Get main headers
- headers = re.findall(
- r"^### (?:\d+\.\s*)?(.+)$", answer_section, re.MULTILINE
+ if "Charakterystyka" in header:
+ stripped = header.replace("Charakterystyka", "").strip()
+ return f"Scharakteryzuj: {stripped}"
+ if header.endswith("?"):
+ return header
+ return f"Omów: {header}"
+
+
+def _extract_body_parts(body: str) -> list[str]:
+ """Extract structured answer parts from a section body."""
+ answer_parts: list[str] = []
+
+ subheaders = re.findall(r"^#### (.+)$", body, re.MULTILINE)
+ if subheaders:
+ answer_parts.extend(subheaders[:4])
+
+ bullets = re.findall(
+ r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]+)?", body
+ )
+ for term, desc in bullets[:5]:
+ if desc:
+ answer_parts.append(
+ f"{term}: {desc.strip()}"
)
- headers = [h.strip() for h in headers if len(h.strip()) > 3][:6]
+ else:
+ answer_parts.append(f"{term}")
- if headers:
- answer_html = "Kluczowe zagadnienia:" + format_list(headers)
- cards.append(
- {
- "front": clean_text(main_q),
- "back": answer_html,
- "tags": f"{base_tags} pytanie_glowne",
- }
- )
+ if not answer_parts:
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip()
+ and not p.strip().startswith("```")
+ and not p.strip().startswith("|")
+ ]
+ if paras:
+ first = paras[0]
+ if len(first) > MAX_CONTENT_LENGTH:
+ first = first[:MAX_CONTENT_LENGTH] + "..."
+ answer_parts.append(first)
- # =====================================================
- # CARD TYPE 2: Subsection Cards (detailed concepts)
- # =====================================================
- # Find all ### sections
+ return answer_parts
+
+
+def _extract_subsection_cards(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Extract subsection detail cards."""
+ cards: list[dict[str, str]] = []
sections = re.findall(
- r"^### (?:\d+\.\s*)?(.+?)\n((?:(?!^###).)+)", content, re.MULTILINE | re.DOTALL
+ r"^### (?:\d+\.\s*)?(.+?)\n((?:(?!^###).)+)",
+ content,
+ re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
- body = body.strip()
+ for raw_header, raw_body in sections:
+ header = raw_header.strip()
+ body = raw_body.strip()
- # Skip very short sections or example sections
- if len(body) < 50 or header.lower().startswith("przykład"):
+ if (
+ len(body) < MIN_BODY_LENGTH
+ or header.lower().startswith("przykład")
+ ):
continue
- # Extract key information from body
- answer_parts = []
-
- # Look for #### sub-headers
- subheaders = re.findall(r"^#### (.+)$", body, re.MULTILINE)
- if subheaders:
- answer_parts.extend(subheaders[:4])
-
- # Look for bullet points with bold terms
- bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]+)?", body)
- for term, desc in bullets[:5]:
- if desc:
- answer_parts.append(f"{term}: {desc.strip()}")
- else:
- answer_parts.append(f"{term}")
-
- # If no structured content, get first paragraph
- if not answer_parts:
- paras = [
- p.strip()
- for p in body.split("\n\n")
- if p.strip()
- and not p.strip().startswith("```")
- and not p.strip().startswith("|")
- ]
- if paras:
- first = paras[0]
- # Limit length
- if len(first) > 300:
- first = first[:300] + "..."
- answer_parts.append(first)
+ answer_parts = _extract_body_parts(body)
if answer_parts:
- # Determine card type
- if "Definicja" in header or "Co to" in header:
- q = f"Co to jest: {header.replace('Definicja', '').strip()}?"
- elif "Charakterystyka" in header:
- q = f"Scharakteryzuj: {header.replace('Charakterystyka', '').strip()}"
- elif header.endswith("?"):
- q = header
- else:
- q = f"Omów: {header}"
-
- # Format answer
+ question = _make_question_text(header)
if len(answer_parts) > 1:
answer_html = format_list(answer_parts)
else:
@@ -163,15 +203,20 @@ def extract_from_file(filepath) -> list[dict[str, str]]:
cards.append(
{
- "front": clean_text(q),
+ "front": clean_text(question),
"back": answer_html,
"tags": f"{base_tags} szczegoly",
}
)
- # =====================================================
- # CARD TYPE 3: Algorithms/Formulas
- # =====================================================
+ return cards
+
+
+def _extract_algo_cards(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Extract algorithm/formula cards."""
+ cards: list[dict[str, str]] = []
algo_patterns = [
r"#### Złożoność(?:\s+czasowa)?\s*\n(.+?)(?=\n####|\n###|\Z)",
r"Złożoność:\s*\*\*([^*]+)\*\*",
@@ -179,85 +224,137 @@ def extract_from_file(filepath) -> list[dict[str, str]]:
for pattern in algo_patterns:
matches = re.findall(pattern, content, re.DOTALL)
- for match in matches[:2]:
- if len(match) > 10:
- # Find context - which algorithm?
+ for algo_match in matches[:2]:
+ if len(algo_match) > MIN_MATCH_LENGTH:
algo_context = re.search(
- r"### (\d+\.\s*)?(.+?)(?=\n)", content[: content.find(match)]
+ r"### (\d+\.\s*)?(.+?)(?=\n)",
+ content[: content.find(algo_match)],
)
if algo_context:
algo_name = algo_context.group(2).strip()
cards.append(
{
- "front": f"Jaka jest złożoność algorytmu/metody: {algo_name}?",
- "back": clean_text(match.strip()[:200]),
+ "front": (
+ "Jaka jest złożoność"
+ f" algorytmu/metody: {algo_name}?"
+ ),
+ "back": clean_text(
+ algo_match.strip()[:200]
+ ),
"tags": f"{base_tags} zlozonosc",
}
)
break
- # =====================================================
- # CARD TYPE 4: Comparisons (when file contains comparisons)
- # =====================================================
+ return cards
+
+
+def _extract_comparison_cards(
+ content: str, base_tags: str, num: str,
+) -> list[dict[str, str]]:
+ """Extract comparison cards."""
compare_match = re.search(
r"## .*(Porównanie|Zestawienie|vs).*\n(.+?)(?=\n## |\Z)",
content,
re.DOTALL | re.IGNORECASE,
)
- if compare_match:
- compare_section = compare_match.group(2)
- # Extract comparison items
- items = re.findall(r"\|\s*\*\*([^|*]+)\*\*\s*\|([^|]+)\|", compare_section)
- if items:
- comparison_html = "
| Aspekt | Wartość |
"
- for aspect, value in items[:6]:
- comparison_html += f"| {clean_text(aspect)} | {clean_text(value)} |
"
- comparison_html += "
"
+ if not compare_match:
+ return []
- # Get comparison title
- title_match = re.search(
- r"## .*(Porównanie|Zestawienie).*?(\w+.*?(?:vs|i|oraz).*?\w+)",
- compare_match.group(0),
- re.IGNORECASE,
- )
- if title_match:
- cards.append(
- {
- "front": f"Porównaj kluczowe różnice w temacie: pytanie {num}",
- "back": comparison_html,
- "tags": f"{base_tags} porownanie",
- }
- )
+ compare_section = compare_match.group(2)
+ items = re.findall(
+ r"\|\s*\*\*([^|*]+)\*\*\s*\|([^|]+)\|",
+ compare_section,
+ )
+ if not items:
+ return []
- # =====================================================
- # CARD TYPE 5: Q&A from practice questions section
- # =====================================================
- qa_section = re.search(r"## 🎓 Pytania.*?\n(.+?)(?=\n## |\Z)", content, re.DOTALL)
- if qa_section:
- qa_content = qa_section.group(1)
- # Find Q&A pairs
- qas = re.findall(
- r'### Q\d+:?\s*["\']?(.+?)["\']?\s*\n.*?Odpowiedź:\s*\n?(.+?)(?=\n### |\Z)',
- qa_content,
- re.DOTALL,
+ comparison_html = (
+ "| Aspekt | Wartość |
"
+ )
+ for aspect, value in items[:MAX_COMPARISON_ITEMS]:
+ comparison_html += (
+ f"| {clean_text(aspect)} | "
+ f"{clean_text(value)} |
"
)
- for q, a in qas[:3]:
- q = re.sub(r"\s+", " ", q.strip())
- a = a.strip()
- if len(a) > 30:
- # Limit answer length
- a_lines = a.split("\n")
- a_short = "\n".join(a_lines[:5])
- if len(a_short) > 400:
- a_short = a_short[:400] + "..."
+ comparison_html += "
"
- cards.append(
- {
- "front": clean_text(q),
- "back": clean_text(a_short).replace("\n", "
"),
- "tags": f"{base_tags} egzamin_praktyka",
- }
- )
+ title_match = re.search(
+ r"## .*(Porównanie|Zestawienie)"
+ r".*?(\w+.*?(?:vs|i|oraz).*?\w+)",
+ compare_match.group(0),
+ re.IGNORECASE,
+ )
+ if not title_match:
+ return []
+
+ return [
+ {
+ "front": (
+ "Porównaj kluczowe różnice"
+ f" w temacie: pytanie {num}"
+ ),
+ "back": comparison_html,
+ "tags": f"{base_tags} porownanie",
+ }
+ ]
+
+
+def _extract_qa_cards(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Extract Q&A practice cards."""
+ cards: list[dict[str, str]] = []
+ qa_section = re.search(
+ r"## 🎓 Pytania.*?\n(.+?)(?=\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if not qa_section:
+ return cards
+
+ qa_content = qa_section.group(1)
+ qas = re.findall(
+ r"### Q\d+:?\s*[\"']?(.+?)[\"']?\s*\n"
+ r".*?Odpowiedź:\s*\n?(.+?)(?=\n### |\Z)",
+ qa_content,
+ re.DOTALL,
+ )
+ for raw_q, raw_a in qas[:3]:
+ question = re.sub(r"\s+", " ", raw_q.strip())
+ answer = raw_a.strip()
+ if len(answer) > MIN_QA_LENGTH:
+ a_lines = answer.split("\n")
+ a_short = "\n".join(a_lines[:5])
+ if len(a_short) > MAX_ANSWER_LENGTH:
+ a_short = a_short[:MAX_ANSWER_LENGTH] + "..."
+
+ cards.append(
+ {
+ "front": clean_text(question),
+ "back": clean_text(a_short).replace(
+ "\n", "
"
+ ),
+ "tags": f"{base_tags} egzamin_praktyka",
+ }
+ )
+
+ return cards
+
+
+def extract_from_file(filepath: str) -> list[dict[str, str]]:
+ """Extract flashcard data from a markdown file."""
+ num, subject, content = _get_file_metadata(filepath)
+ base_tags = f"egzamin_magisterski pyt{num} {subject}"
+
+ cards: list[dict[str, str]] = []
+ cards.extend(_extract_main_question_card(content, base_tags))
+ cards.extend(_extract_subsection_cards(content, base_tags))
+ cards.extend(_extract_algo_cards(content, base_tags))
+ cards.extend(
+ _extract_comparison_cards(content, base_tags, num)
+ )
+ cards.extend(_extract_qa_cards(content, base_tags))
return cards
@@ -272,13 +369,13 @@ def main() -> None:
all_cards = []
for md_file in sorted(odpowiedzi_dir.glob("*.md")):
- print(f"Processing: {md_file.name}", end=" ")
+ logger.info("Processing: %s", md_file.name)
try:
cards = extract_from_file(md_file)
all_cards.extend(cards)
- print(f"→ {len(cards)} cards")
- except Exception as e:
- print(f"→ ERROR: {e}")
+ logger.info(" -> %d cards", len(cards))
+ except (ValueError, OSError) as e:
+ logger.info(" -> ERROR: %s", e)
# Remove potential duplicates (same front)
seen = set()
@@ -306,23 +403,25 @@ def main() -> None:
f.write(f"{front}\t{back}\t{tags}\n")
- print(f"\n{'=' * 50}")
- print(f"✅ Generated {len(unique_cards)} unique flashcards")
- print(f"📁 Saved to: {output_file}")
- print(f"{'=' * 50}")
- print("\n📋 IMPORT INSTRUCTIONS:")
- print("─" * 40)
- print("Anki Desktop:")
- print(" 1. File → Import")
- print(" 2. Select: anki_egzamin_magisterski.txt")
- print(" 3. Verify: Fields separated by Tab")
- print(" 4. Check: Allow HTML in fields")
- print(" 5. Click Import")
- print()
- print("AnkiWeb / AnkiDroid:")
- print(" 1. First import on Anki Desktop")
- print(" 2. Click Sync to upload to AnkiWeb")
- print(" 3. Sync on mobile to download")
+ logger.info("=" * 50)
+ logger.info(
+ "Generated %d unique flashcards", len(unique_cards)
+ )
+ logger.info("Saved to: %s", output_file)
+ logger.info("=" * 50)
+ logger.info("IMPORT INSTRUCTIONS:")
+ logger.info("-" * 40)
+ logger.info("Anki Desktop:")
+ logger.info(" 1. File -> Import")
+ logger.info(" 2. Select: anki_egzamin_magisterski.txt")
+ logger.info(" 3. Verify: Fields separated by Tab")
+ logger.info(" 4. Check: Allow HTML in fields")
+ logger.info(" 5. Click Import")
+ logger.info("")
+ logger.info("AnkiWeb / AnkiDroid:")
+ logger.info(" 1. First import on Anki Desktop")
+ logger.info(" 2. Click Sync to upload to AnkiWeb")
+ logger.info(" 3. Sync on mobile to download")
if __name__ == "__main__":
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
index c659fc2..fe4425b 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
@@ -6,12 +6,16 @@ Creates a tab-separated file compatible with Anki import.
from __future__ import annotations
+import logging
from pathlib import Path
import re
-import traceback
+
+logger = logging.getLogger(__name__)
+
+MIN_HEADER_WORDS = 3
-def extract_main_question(content, filename) -> str:
+def extract_main_question(content: str, filename: str) -> str:
"""Extract the main exam question from the file."""
# Extract the main question from ## Pytanie section
question_match = re.search(
@@ -26,13 +30,13 @@ def extract_main_question(content, filename) -> str:
return title_match.group(1) if title_match else filename
-def extract_subject(content) -> str:
+def extract_subject(content: str) -> str:
"""Extract the subject code."""
subject_match = re.search(r"Przedmiot:\s*(\w+)", content)
return subject_match.group(1) if subject_match else "Ogólne"
-def extract_key_points(content) -> list[str]:
+def extract_key_points(content: str) -> list[str]:
"""Extract key points from the main answer section."""
points = []
@@ -51,14 +55,14 @@ def extract_key_points(content) -> list[str]:
headers = re.findall(r"^### (.+)$", answer_text, re.MULTILINE)
for h in headers[:6]:
# Clean header
- h = re.sub(r"\d+\.\s*", "", h).strip()
- if h and len(h) > 3:
- points.append(h)
+ cleaned = re.sub(r"\d+\.\s*", "", h).strip()
+ if cleaned and len(cleaned) > MIN_HEADER_WORDS:
+ points.append(cleaned)
return points
-def extract_definitions(content) -> list[tuple[str, str]]:
+def extract_definitions(content: str) -> list[tuple[str, str]]:
"""Extract key definitions from the content."""
definitions = []
@@ -66,9 +70,9 @@ def extract_definitions(content) -> list[tuple[str, str]]:
pattern = r"\*\*([^*\n]+)\*\*\s*[--:]\s*([^*\n]{20,150})"
matches = re.findall(pattern, content)
- for term, definition in matches:
- term = term.strip()
- definition = definition.strip()
+ for raw_term, raw_def in matches:
+ term = raw_term.strip()
+ definition = raw_def.strip()
# Filter out non-definition patterns
if (
term
@@ -81,7 +85,7 @@ def extract_definitions(content) -> list[tuple[str, str]]:
return definitions[:5]
-def clean_html(text) -> str:
+def clean_html(text: str) -> str:
"""Convert markdown to HTML and clean for Anki."""
if not text:
return ""
@@ -101,7 +105,7 @@ def clean_html(text) -> str:
return text.strip()
-def process_file(filepath) -> list[dict[str, str]]:
+def process_file(filepath: str) -> list[dict[str, str]]:
"""Process a single file and return flashcards."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
@@ -111,11 +115,7 @@ def process_file(filepath) -> list[dict[str, str]]:
# Extract metadata
filename = Path(filepath).name
match = re.match(r"(\d+)-(.+)\.md", filename)
- if match:
- num = match.group(1)
- match.group(2).replace("-", "_")
- else:
- num = "00"
+ num = match.group(1) if match else "00"
subject = extract_subject(content)
main_question = extract_main_question(content, filename)
@@ -156,14 +156,13 @@ def main() -> None:
# Process each file
for md_file in sorted(odpowiedzi_dir.glob("*.md")):
- print(f"Processing: {md_file.name}")
+ logger.info("Processing: %s", md_file.name)
try:
cards = process_file(md_file)
all_cards.extend(cards)
- print(f" -> {len(cards)} cards")
- except Exception as e:
- print(f" -> Error: {e}")
- traceback.print_exc()
+ logger.info(" -> %d cards", len(cards))
+ except (ValueError, OSError):
+ logger.exception(" -> Error processing file")
# Write Anki-compatible file
with Path(output_file).open("w", encoding="utf-8") as f:
@@ -186,16 +185,22 @@ def main() -> None:
f.write(f"{front}\t{back}\t{tags}\n")
- print(f"\n✅ Created {len(all_cards)} flashcards")
- print(f"📁 Output: {output_file}")
- print("\n=== Import Instructions ===")
- print("1. Open Anki desktop → File → Import")
- print("2. Select: anki_egzamin_magisterski.txt")
- print("3. Set 'Fields separated by: Tab'")
- print("4. Check 'Allow HTML in fields'")
- print("5. Map: Field 1 → Front, Field 2 → Back, Field 3 → Tags")
- print("6. Click Import")
- print("\nFor AnkiWeb/AnkiDroid: Sync after importing on desktop")
+ logger.info("Created %d flashcards", len(all_cards))
+ logger.info("Output: %s", output_file)
+ logger.info("=== Import Instructions ===")
+ logger.info("1. Open Anki desktop -> File -> Import")
+ logger.info("2. Select: anki_egzamin_magisterski.txt")
+ logger.info("3. Set 'Fields separated by: Tab'")
+ logger.info("4. Check 'Allow HTML in fields'")
+ logger.info(
+ "5. Map: Field 1 -> Front, Field 2 -> Back,"
+ " Field 3 -> Tags"
+ )
+ logger.info("6. Click Import")
+ logger.info(
+ "For AnkiWeb/AnkiDroid:"
+ " Sync after importing on desktop"
+ )
if __name__ == "__main__":
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
index 148729c..f86effd 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
@@ -3,11 +3,18 @@
from __future__ import annotations
+import logging
from pathlib import Path
import re
+logger = logging.getLogger(__name__)
-def clean_text(text) -> str:
+MIN_PARA_LENGTH = 20
+MAX_PARA_LENGTH = 400
+MIN_BODY_LENGTH = 80
+
+
+def clean_text(text: str) -> str:
"""Clean text for Anki."""
if not text:
return ""
@@ -19,7 +26,7 @@ def clean_text(text) -> str:
return text.strip()
-def extract_real_answer(content, section_name) -> str | None:
+def extract_real_answer(content: str, section_name: str) -> str | None:
"""Extract actual content from a section, not just headers."""
# Find the section
pattern = rf"### (?:\d+\.\s*)?{re.escape(section_name)}\s*\n((?:(?!^### ).)+)"
@@ -52,19 +59,21 @@ def extract_real_answer(content, section_name) -> str | None:
for p in body.split("\n\n")
if p.strip() and not p.startswith("```") and not p.startswith("|")
]
- for p in paras[:2]:
- if len(p) > 20 and len(p) < 400:
- lines.append(p)
+ lines.extend(
+ p for p in paras[:2]
+ if len(p) > MIN_PARA_LENGTH and len(p) < MAX_PARA_LENGTH
+ )
return "
".join(lines[:6]) if lines else None
-def extract_cards(filepath) -> list[dict[str, str]]:
- """Extract flashcards from a file."""
+def _read_file_metadata(
+ filepath: str | Path,
+) -> tuple[str, str, str | None]:
+ """Read file and extract metadata."""
with Path(filepath).open(encoding="utf-8") as f:
content = f.read()
- cards = []
filename = Path(filepath).name
match = re.match(r"(\d+)-(.+)\.md", filename)
num = match.group(1) if match else "00"
@@ -73,182 +82,228 @@ def extract_cards(filepath) -> list[dict[str, str]]:
subject = subj_match.group(1) if subj_match else "Ogólne"
base_tags = f"egzamin_magisterski pyt{num} {subject}"
- # Get main question
q_match = re.search(
- r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*',
+ content,
+ re.DOTALL,
+ )
+ main_question = (
+ re.sub(r"\s+", " ", q_match.group(1).strip()) if q_match else None
)
- main_question = re.sub(r"\s+", " ", q_match.group(1).strip()) if q_match else None
- # ===============================================
- # MAIN CARD: Question with REAL answer summary
- # ===============================================
- if main_question:
- # Build a real answer from the main sections
- answer_parts = []
+ return content, base_tags, main_question
- # For automata question - extract key facts about each automaton
- if "automat" in main_question.lower() or "maszyn" in main_question.lower():
- # FA
- fa_match = re.search(
- r"Automat Skończony.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
- content,
- re.DOTALL,
+
+def _extract_automata_facts(content: str) -> list[str]:
+ """Extract automata-specific facts."""
+ parts: list[str] = []
+ automata = [
+ ("Automat Skończony", "FA"),
+ ("Automat ze Stosem", "PDA"),
+ ("Maszyna Turinga", "TM"),
+ ]
+ for name, abbrev in automata:
+ pattern = (
+ rf"{name}.*?Rozpoznawana klasa języków"
+ r"\s*\n\s*\*\*([^*]+)\*\*"
+ )
+ match = re.search(pattern, content, re.DOTALL)
+ if match:
+ parts.append(
+ f"{name} ({abbrev}): "
+ f"{match.group(1).strip()}"
)
- if fa_match:
- answer_parts.append(
- f"Automat Skończony (FA): {fa_match.group(1).strip()}"
- )
+ return parts
- # PDA
- pda_match = re.search(
- r"Automat ze Stosem.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
- content,
- re.DOTALL,
- )
- if pda_match:
- answer_parts.append(
- f"Automat ze Stosem (PDA): {pda_match.group(1).strip()}"
- )
- # TM
- tm_match = re.search(
- r"Maszyna Turinga.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
- content,
- re.DOTALL,
- )
- if tm_match:
- answer_parts.append(
- f"Maszyna Turinga (TM): {tm_match.group(1).strip()}"
- )
+def _extract_generic_facts(content: str) -> list[str]:
+ """Extract generic definitions and summaries."""
+ parts: list[str] = []
+ key_patterns = [
+ r"#### Definicja\s*\n([^\n#]+)",
+ r"#### Charakterystyka\s*\n([^\n#]+)",
+ r"\*\*Definicja[:\s]*\*\*\s*([^\n]+)",
+ ]
+ for pattern in key_patterns:
+ parts.extend(
+ found.strip()
+ for found in re.findall(pattern, content)[:3]
+ if len(found) > MIN_PARA_LENGTH
+ )
+ return parts
- # Generic extraction if specific didn't work
- if not answer_parts:
- # Look for key definitions/summaries
- key_patterns = [
- r"#### Definicja\s*\n([^\n#]+)",
- r"#### Charakterystyka\s*\n([^\n#]+)",
- r"\*\*Definicja[:\s]*\*\*\s*([^\n]+)",
- ]
- for pattern in key_patterns:
- for match in re.findall(pattern, content)[:3]:
- if len(match) > 20:
- answer_parts.append(match.strip())
- # Still nothing? Get first substantive paragraph from main answer
- if not answer_parts:
- main_answer = re.search(
- r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)", content, re.DOTALL
- )
- if main_answer:
- # Skip headers, get actual content
- text = main_answer.group(1)
- paras = re.findall(r"\n\n([^#\n][^\n]{50,300})", text)
- answer_parts = paras[:3]
+def _extract_first_paragraphs(content: str) -> list[str]:
+ """Extract first substantive paragraphs from main answer."""
+ main_answer = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if not main_answer:
+ return []
+ text = main_answer.group(1)
+ paras = re.findall(r"\n\n([^#\n][^\n]{50,300})", text)
+ return paras[:3]
- if answer_parts:
- answer = "
".join([clean_text(p) for p in answer_parts])
- cards.append(
- {
- "front": clean_text(main_question),
- "back": answer,
- "tags": f"{base_tags} pytanie_glowne",
- }
+
+def _build_main_card(
+ content: str,
+ main_question: str | None,
+ base_tags: str,
+) -> dict[str, str] | None:
+ """Build the main question card."""
+ if not main_question:
+ return None
+
+ answer_parts: list[str] = []
+ if (
+ "automat" in main_question.lower()
+ or "maszyn" in main_question.lower()
+ ):
+ answer_parts = _extract_automata_facts(content)
+
+ if not answer_parts:
+ answer_parts = _extract_generic_facts(content)
+
+ if not answer_parts:
+ answer_parts = _extract_first_paragraphs(content)
+
+ if not answer_parts:
+ return None
+
+ answer = "
".join(
+ clean_text(p) for p in answer_parts
+ )
+ return {
+ "front": clean_text(main_question),
+ "back": answer,
+ "tags": f"{base_tags} pytanie_glowne",
+ }
+
+
+def _extract_section_content(body: str) -> list[str]:
+ """Extract content lines from a section body."""
+ answer_lines: list[str] = []
+
+ def_match = re.search(
+ r"#### Definicja[^\n]*\n([^\n#]+(?:\n[^\n#]+)?)", body,
+ )
+ if def_match:
+ answer_lines.append(def_match.group(1).strip())
+
+ char_match = re.search(
+ r"#### Charakterystyka\s*\n((?:[-•][^\n]+\n?)+)", body,
+ )
+ if char_match:
+ bullets = re.findall(
+ r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)",
+ char_match.group(1),
+ )
+ for term, desc in bullets[:4]:
+ answer_lines.append(
+ f"• {term}: {desc.strip()}"
+ if desc
+ else f"• {term}"
)
- # ===============================================
- # CONCEPT CARDS: Specific topics with real content
- # ===============================================
- # Find all ### sections and extract their actual content
+ if not answer_lines:
+ bullets = re.findall(
+ r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)", body,
+ )
+ for term, desc in bullets[:5]:
+ answer_lines.append(
+ f"• {term}: {desc.strip()}"
+ if desc
+ else f"• {term}"
+ )
+
+ if not answer_lines:
+ first_para = re.search(
+ r"^([^#\n\-•|`][^\n]{30,250})", body, re.MULTILINE,
+ )
+ if first_para:
+ answer_lines.append(first_para.group(1))
+
+ return answer_lines
+
+
+def _build_concept_cards(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Build concept cards from ### sections."""
+ cards: list[dict[str, str]] = []
sections = re.findall(
r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
content,
re.MULTILINE | re.DOTALL,
)
- for header, body in sections:
- header = header.strip()
- body = body.strip()
+ for raw_header, raw_body in sections:
+ header = raw_header.strip()
+ body = raw_body.strip()
- # Skip short sections, mnemonics, examples
if (
- len(body) < 80
+ len(body) < MIN_BODY_LENGTH
or "Przykład" in header
or "Mnemonic" in header
or '"' in header
):
continue
- # Extract real content
- answer_lines = []
-
- # Get definition if present
- def_match = re.search(r"#### Definicja[^\n]*\n([^\n#]+(?:\n[^\n#]+)?)", body)
- if def_match:
- answer_lines.append(def_match.group(1).strip())
-
- # Get characterization
- char_match = re.search(r"#### Charakterystyka\s*\n((?:[-•][^\n]+\n?)+)", body)
- if char_match:
- bullets = re.findall(
- r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)", char_match.group(1)
- )
- for term, desc in bullets[:4]:
- answer_lines.append(
- f"• {term}: {desc.strip()}" if desc else f"• {term}"
- )
-
- # Get bullet points if no structured content yet
+ answer_lines = _extract_section_content(body)
if not answer_lines:
- bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)", body)
- for term, desc in bullets[:5]:
- answer_lines.append(
- f"• {term}: {desc.strip()}" if desc else f"• {term}"
- )
+ continue
- # Get first paragraph if still nothing
- if not answer_lines:
- first_para = re.search(r"^([^#\n\-•|`][^\n]{30,250})", body, re.MULTILINE)
- if first_para:
- answer_lines.append(first_para.group(1))
+ question = (
+ header if header.endswith("?") else f"Wyjaśnij: {header}"
+ )
+ answer = "
".join(
+ clean_text(line) for line in answer_lines
+ )
+ cards.append(
+ {
+ "front": clean_text(question),
+ "back": answer,
+ "tags": f"{base_tags} szczegoly",
+ }
+ )
- if answer_lines:
- question = f"Wyjaśnij: {header}" if not header.endswith("?") else header
- answer = "
".join([clean_text(l) for l in answer_lines])
+ return cards
- cards.append(
- {
- "front": clean_text(question),
- "back": answer,
- "tags": f"{base_tags} szczegoly",
- }
- )
- # ===============================================
- # Q&A CARDS: From practice questions section
- # ===============================================
+def _build_qa_cards(
+ content: str, base_tags: str,
+) -> list[dict[str, str]]:
+ """Build Q&A practice cards."""
+ cards: list[dict[str, str]] = []
qa_matches = re.findall(
- r'### Q\d+:\s*["\']?([^"\'?\n]+)\?*["\']?\s*\n.*?Odpowiedź:\s*\n(.+?)(?=\n### |\n## |\Z)',
+ r'### Q\d+:\s*["\']?([^"\'?\n]+)\?*["\']?\s*\n'
+ r".*?Odpowiedź:\s*\n(.+?)(?=\n### |\n## |\Z)",
content,
re.DOTALL,
)
- for question, answer in qa_matches[:5]:
- question = question.strip()
- answer = answer.strip()
+ for raw_question, raw_answer in qa_matches[:5]:
+ question = raw_question.strip()
+ answer_text = raw_answer.strip()
- # Clean up answer - get first meaningful part
- answer_lines = answer.split("\n")
- clean_answer = []
- for line in answer_lines[:6]:
- line = line.strip()
- if line and not line.startswith("```") and not line.startswith("|"):
- clean_answer.append(line)
+ answer_lines = answer_text.split("\n")
+ clean_answer = [
+ stripped
+ for raw_line in answer_lines[:6]
+ if (stripped := raw_line.strip())
+ and not stripped.startswith("```")
+ and not stripped.startswith("|")
+ ]
if clean_answer:
cards.append(
{
"front": clean_text(question + "?"),
- "back": "
".join([clean_text(l) for l in clean_answer]),
+ "back": "
".join(
+ clean_text(line) for line in clean_answer
+ ),
"tags": f"{base_tags} qa",
}
)
@@ -256,6 +311,20 @@ def extract_cards(filepath) -> list[dict[str, str]]:
return cards
+def extract_cards(filepath: str | Path) -> list[dict[str, str]]:
+ """Extract flashcards from a file."""
+ content, base_tags, main_question = _read_file_metadata(filepath)
+
+ cards: list[dict[str, str]] = []
+ main_card = _build_main_card(content, main_question, base_tags)
+ if main_card:
+ cards.append(main_card)
+
+ cards.extend(_build_concept_cards(content, base_tags))
+ cards.extend(_build_qa_cards(content, base_tags))
+ return cards
+
+
def main() -> None:
"""Main."""
odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
@@ -266,13 +335,13 @@ def main() -> None:
all_cards = []
for md_file in sorted(odpowiedzi_dir.glob("*.md")):
- print(f"Processing: {md_file.name}", end=" ")
+ logger.info("Processing: %s", md_file.name)
try:
cards = extract_cards(md_file)
all_cards.extend(cards)
- print(f"→ {len(cards)} cards")
- except Exception as e:
- print(f"→ ERROR: {e}")
+ logger.info(" -> %d cards", len(cards))
+ except (ValueError, OSError):
+ logger.exception(" -> Error processing file")
# Remove duplicates
seen = set()
@@ -299,8 +368,12 @@ def main() -> None:
tags = card["tags"]
f.write(f"{front}\t{back}\t{tags}\n")
- print(f"\n✅ Generated {len(unique_cards)} flashcards")
- print(f"📁 Output: {output_file}")
+ logger.info(
+ "Generated %d unique cards from %d total",
+ len(unique_cards),
+ len(all_cards),
+ )
+ logger.info("Output: %s", output_file)
if __name__ == "__main__":
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
index c07f7ba..54353f8 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
@@ -9,14 +9,25 @@
All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING
+
import matplotlib as mpl
mpl.use("Agg")
-from pathlib import Path
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+
+logger = logging.getLogger(__name__)
+
DPI = 300
BG = "white"
LN = "black"
@@ -36,26 +47,82 @@ LIGHT_RED = "#F8D7DA"
LIGHT_BLUE = "#D6EAF8"
LIGHT_YELLOW = "#FFF9C4"
+INNER_RATIO = 0.82
+ARROW_OFFSET = 0.4
+LOOP_RAD = 1.8
+LOOP_OFFSET = 0.12
+LOOP_LABEL_OFFSET = 0.35
+MUTATION_SCALE = 12
+HEAD_MARKER_FONTSIZE = 8
+
+
+@dataclass(frozen=True)
+class StateStyle:
+ """Optional styling for automaton state circles."""
+
+ accepting: bool = False
+ initial: bool = False
+ fillcolor: str = "white"
+ fontsize: float = FS
+
+
+@dataclass(frozen=True)
+class ArrowStyle:
+ """Optional styling for curved arrows."""
+
+ connectionstyle: str = "arc3,rad=0.3"
+ fontsize: float = FS_SMALL
+ label_offset: tuple[float, float] = (0, 0)
+
+
+@dataclass(frozen=True)
+class LoopStyle:
+ """Optional styling for self-loops."""
+
+ direction: str = "top"
+ fontsize: float = FS_SMALL
+
def draw_state_circle(
- ax, x, y, r, label, accepting=False, initial=False, fillcolor="white", fontsize=FS
+ ax: Axes,
+ pos: tuple[float, float],
+ r: float,
+ label: str,
+ style: StateStyle | None = None,
) -> None:
"""Draw an automaton state circle."""
+ s = style or StateStyle()
+ x, y = pos
circle = plt.Circle(
- (x, y), r, fill=True, facecolor=fillcolor, edgecolor=LN, linewidth=1.5, zorder=3
+ (x, y),
+ r,
+ fill=True,
+ facecolor=s.fillcolor,
+ edgecolor=LN,
+ linewidth=1.5,
+ zorder=3,
)
ax.add_patch(circle)
- if accepting:
+ if s.accepting:
inner = plt.Circle(
- (x, y), r * 0.82, fill=False, edgecolor=LN, linewidth=1.2, zorder=3
+ (x, y),
+ r * INNER_RATIO,
+ fill=False,
+ edgecolor=LN,
+ linewidth=1.2,
+ zorder=3,
)
ax.add_patch(inner)
- if initial:
+ if s.initial:
ax.annotate(
"",
xy=(x - r, y),
- xytext=(x - r - 0.4, y),
- arrowprops={"arrowstyle": "->", "color": LN, "lw": 1.5},
+ xytext=(x - r - ARROW_OFFSET, y),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": LN,
+ "lw": 1.5,
+ },
zorder=4,
)
ax.text(
@@ -64,25 +131,23 @@ def draw_state_circle(
label,
ha="center",
va="center",
- fontsize=fontsize,
+ fontsize=s.fontsize,
fontweight="bold",
zorder=5,
)
def draw_curved_arrow(
- ax,
- x1,
- y1,
- x2,
- y2,
- label,
- _r=0.25,
- connectionstyle="arc3,rad=0.3",
- fontsize=FS_SMALL,
- label_offset=(0, 0),
+ ax: Axes,
+ start: tuple[float, float],
+ end: tuple[float, float],
+ label: str,
+ style: ArrowStyle | None = None,
) -> None:
"""Draw a curved arrow between points with label."""
+ s = style or ArrowStyle()
+ x1, y1 = start
+ x2, y2 = end
ax.annotate(
"",
xy=(x2, y2),
@@ -91,19 +156,19 @@ def draw_curved_arrow(
"arrowstyle": "->",
"color": LN,
"lw": 1.2,
- "connectionstyle": connectionstyle,
+ "connectionstyle": s.connectionstyle,
},
zorder=2,
)
- mx = (x1 + x2) / 2 + label_offset[0]
- my = (y1 + y2) / 2 + label_offset[1]
+ mx = (x1 + x2) / 2 + s.label_offset[0]
+ my = (y1 + y2) / 2 + s.label_offset[1]
ax.text(
mx,
my,
label,
ha="center",
va="center",
- fontsize=fontsize,
+ fontsize=s.fontsize,
fontstyle="italic",
zorder=5,
bbox={
@@ -115,15 +180,23 @@ def draw_curved_arrow(
)
-def draw_self_loop(ax, x, y, r, label, direction="top", fontsize=FS_SMALL) -> None:
+def draw_self_loop(
+ ax: Axes,
+ pos: tuple[float, float],
+ r: float,
+ label: str,
+ style: LoopStyle | None = None,
+) -> None:
"""Draw a self-loop on a state."""
- if direction == "top":
+ s = style or LoopStyle()
+ x, y = pos
+ if s.direction == "top":
loop = mpatches.FancyArrowPatch(
- (x - 0.12, y + r),
- (x + 0.12, y + r),
- connectionstyle="arc3,rad=-1.8",
+ (x - LOOP_OFFSET, y + r),
+ (x + LOOP_OFFSET, y + r),
+ connectionstyle=f"arc3,rad=-{LOOP_RAD}",
arrowstyle="->",
- mutation_scale=12,
+ mutation_scale=MUTATION_SCALE,
lw=1.2,
color=LN,
zorder=2,
@@ -131,21 +204,21 @@ def draw_self_loop(ax, x, y, r, label, direction="top", fontsize=FS_SMALL) -> No
ax.add_patch(loop)
ax.text(
x,
- y + r + 0.35,
+ y + r + LOOP_LABEL_OFFSET,
label,
ha="center",
va="center",
- fontsize=fontsize,
+ fontsize=s.fontsize,
fontstyle="italic",
zorder=5,
)
- elif direction == "bottom":
+ elif s.direction == "bottom":
loop = mpatches.FancyArrowPatch(
- (x - 0.12, y - r),
- (x + 0.12, y - r),
- connectionstyle="arc3,rad=1.8",
+ (x - LOOP_OFFSET, y - r),
+ (x + LOOP_OFFSET, y - r),
+ connectionstyle=f"arc3,rad={LOOP_RAD}",
arrowstyle="->",
- mutation_scale=12,
+ mutation_scale=MUTATION_SCALE,
lw=1.2,
color=LN,
zorder=2,
@@ -153,11 +226,11 @@ def draw_self_loop(ax, x, y, r, label, direction="top", fontsize=FS_SMALL) -> No
ax.add_patch(loop)
ax.text(
x,
- y - r - 0.35,
+ y - r - LOOP_LABEL_OFFSET,
label,
ha="center",
va="center",
- fontsize=fontsize,
+ fontsize=s.fontsize,
fontstyle="italic",
zorder=5,
)
@@ -169,7 +242,10 @@ def draw_self_loop(ax, x, y, r, label, direction="top", fontsize=FS_SMALL) -> No
def draw_fa_recognition() -> None:
"""FA state diagram + step-by-step trace for 'baab'."""
_fig, axes = plt.subplots(
- 1, 2, figsize=(11.69, 4), gridspec_kw={"width_ratios": [1, 1.3]}
+ 1,
+ 2,
+ figsize=(11.69, 4),
+ gridspec_kw={"width_ratios": [1, 1.3]},
)
# --- Left: State diagram ---
@@ -179,69 +255,99 @@ def draw_fa_recognition() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- 'DFA — diagram stanów\nL = {słowa nad {a,b} kończące się na "ab"}',
+ "DFA — diagram stanów\n"
+ 'L = {słowa nad {a,b} kończące się na "ab"}',
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
- R = 0.35
- # States positions
- states = {"q₀": (0.8, 0.5), "q₁": (2.8, 0.5), "q₂": (4.8, 0.5)}
+ state_r = 0.35
+ states = {
+ "q₀": (0.8, 0.5),
+ "q₁": (2.8, 0.5),
+ "q₂": (4.8, 0.5),
+ }
- draw_state_circle(ax, *states["q₀"], R, "q₀", initial=True)
- draw_state_circle(ax, *states["q₁"], R, "q₁")
- draw_state_circle(ax, *states["q₂"], R, "q₂", accepting=True, fillcolor=LIGHT_GREEN)
+ draw_state_circle(
+ ax,
+ states["q₀"],
+ state_r,
+ "q₀",
+ StateStyle(initial=True),
+ )
+ draw_state_circle(ax, states["q₁"], state_r, "q₁")
+ draw_state_circle(
+ ax,
+ states["q₂"],
+ state_r,
+ "q₂",
+ StateStyle(
+ accepting=True, fillcolor=LIGHT_GREEN
+ ),
+ )
# Transitions
# q₀ --a--> q₁
draw_curved_arrow(
ax,
- states["q₀"][0] + R,
- states["q₀"][1] + 0.05,
- states["q₁"][0] - R,
- states["q₁"][1] + 0.05,
+ (states["q₀"][0] + state_r, states["q₀"][1] + 0.05),
+ (states["q₁"][0] - state_r, states["q₁"][1] + 0.05),
"a",
- connectionstyle="arc3,rad=0.15",
- label_offset=(0, 0.25),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, 0.25),
+ ),
)
# q₁ --b--> q₂
draw_curved_arrow(
ax,
- states["q₁"][0] + R,
- states["q₁"][1] + 0.05,
- states["q₂"][0] - R,
- states["q₂"][1] + 0.05,
+ (states["q₁"][0] + state_r, states["q₁"][1] + 0.05),
+ (states["q₂"][0] - state_r, states["q₂"][1] + 0.05),
"b",
- connectionstyle="arc3,rad=0.15",
- label_offset=(0, 0.25),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, 0.25),
+ ),
)
# q₂ --a--> q₁
draw_curved_arrow(
ax,
- states["q₂"][0] - R,
- states["q₂"][1] - 0.05,
- states["q₁"][0] + R,
- states["q₁"][1] - 0.05,
+ (states["q₂"][0] - state_r, states["q₂"][1] - 0.05),
+ (states["q₁"][0] + state_r, states["q₁"][1] - 0.05),
"a",
- connectionstyle="arc3,rad=0.15",
- label_offset=(0, -0.3),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, -0.3),
+ ),
)
# q₂ --b--> q₀
draw_curved_arrow(
ax,
- states["q₂"][0] - 0.2,
- states["q₂"][1] - R,
- states["q₀"][0] + 0.2,
- states["q₀"][1] - R,
+ (states["q₂"][0] - 0.2, states["q₂"][1] - state_r),
+ (states["q₀"][0] + 0.2, states["q₀"][1] - state_r),
"b",
- connectionstyle="arc3,rad=0.4",
- label_offset=(0, -0.4),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.4",
+ label_offset=(0, -0.4),
+ ),
)
# q₀ --b--> q₀ (self-loop)
- draw_self_loop(ax, *states["q₀"], R, "b", direction="top")
+ draw_self_loop(
+ ax,
+ states["q₀"],
+ state_r,
+ "b",
+ LoopStyle(direction="top"),
+ )
# q₁ --a--> q₁ (self-loop)
- draw_self_loop(ax, *states["q₁"], R, "a", direction="top")
+ draw_self_loop(
+ ax,
+ states["q₁"],
+ state_r,
+ "a",
+ LoopStyle(direction="top"),
+ )
# Legend
ax.text(
@@ -251,18 +357,31 @@ def draw_fa_recognition() -> None:
fontsize=FS_SMALL,
ha="left",
va="center",
- bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
# --- Right: Step-by-step trace ---
ax2 = axes[1]
ax2.axis("off")
ax2.set_title(
- 'Ślad wykonania — wejście: "baab"', fontsize=FS_TITLE, fontweight="bold", pad=10
+ 'Ślad wykonania — wejście: "baab"',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
)
trace_data = [
- ["Krok", "Czytam", "Stan przed", "Przejście", "Stan po"],
+ [
+ "Krok",
+ "Czytam",
+ "Stan przed",
+ "Przejście",
+ "Stan po",
+ ],
["—", "—", "q₀ (start)", "—", "q₀"],
["1", "b", "q₀", "δ(q₀, b) = q₀", "q₀"],
["2", "a", "q₀", "δ(q₀, a) = q₁", "q₁"],
@@ -277,7 +396,7 @@ def draw_fa_recognition() -> None:
loc="center",
bbox=[0.05, 0.15, 0.9, 0.75],
)
- table.auto_set_font_size(False)
+ table.auto_set_font_size(auto=False)
table.set_fontsize(FS)
for (row, _col), cell in table.get_celld().items():
cell.set_edgecolor(GRAY3)
@@ -297,7 +416,11 @@ def draw_fa_recognition() -> None:
fontsize=FS + 1,
fontweight="bold",
transform=ax2.transAxes,
- bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": LN,
+ },
)
plt.tight_layout()
@@ -308,16 +431,19 @@ def draw_fa_recognition() -> None:
facecolor=BG,
)
plt.close()
- print(" ✓ fa_recognition_example.png")
+ logger.info(" ✓ fa_recognition_example.png")
# ============================================================
# 2. PDA Recognition Example — PDA for aⁿbⁿ
# ============================================================
def draw_pda_recognition() -> None:
- """PDA state diagram + step-by-step trace with stack visualization for 'aabb'."""
+ """PDA state diagram + step-by-step trace with stack."""
_fig, axes = plt.subplots(
- 1, 2, figsize=(11.69, 5.5), gridspec_kw={"width_ratios": [1, 1.4]}
+ 1,
+ 2,
+ figsize=(11.69, 5.5),
+ gridspec_kw={"width_ratios": [1, 1.4]},
)
# --- Left: State diagram ---
@@ -333,66 +459,107 @@ def draw_pda_recognition() -> None:
pad=10,
)
- R = 0.38
- states = {"q₀": (0.8, 0.5), "q₁": (2.8, 0.5), "q₂": (4.8, 0.5)}
+ state_r = 0.38
+ states = {
+ "q₀": (0.8, 0.5),
+ "q₁": (2.8, 0.5),
+ "q₂": (4.8, 0.5),
+ }
- draw_state_circle(ax, *states["q₀"], R, "q₀", initial=True)
- draw_state_circle(ax, *states["q₁"], R, "q₁")
- draw_state_circle(ax, *states["q₂"], R, "q₂", accepting=True, fillcolor=LIGHT_GREEN)
+ draw_state_circle(
+ ax,
+ states["q₀"],
+ state_r,
+ "q₀",
+ StateStyle(initial=True),
+ )
+ draw_state_circle(ax, states["q₁"], state_r, "q₁")
+ draw_state_circle(
+ ax,
+ states["q₂"],
+ state_r,
+ "q₂",
+ StateStyle(
+ accepting=True, fillcolor=LIGHT_GREEN
+ ),
+ )
# q₀ --b,A/ε--> q₁
draw_curved_arrow(
ax,
- states["q₀"][0] + R,
- states["q₀"][1],
- states["q₁"][0] - R,
- states["q₁"][1],
+ (states["q₀"][0] + state_r, states["q₀"][1]),
+ (states["q₁"][0] - state_r, states["q₁"][1]),
"b, A → ε\n(pop A)",
- connectionstyle="arc3,rad=0.0",
- label_offset=(0, 0.4),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.0",
+ label_offset=(0, 0.4),
+ ),
)
# q₁ --ε,Z₀/Z₀--> q₂
draw_curved_arrow(
ax,
- states["q₁"][0] + R,
- states["q₁"][1],
- states["q₂"][0] - R,
- states["q₂"][1],
+ (states["q₁"][0] + state_r, states["q₁"][1]),
+ (states["q₂"][0] - state_r, states["q₂"][1]),
"ε, Z₀ → Z₀\n(akceptuj)",
- connectionstyle="arc3,rad=0.0",
- label_offset=(0, 0.45),
+ ArrowStyle(
+ connectionstyle="arc3,rad=0.0",
+ label_offset=(0, 0.45),
+ ),
)
# q₀ self-loop: a, Z₀/AZ₀ and a, A/AA
draw_self_loop(
- ax, *states["q₀"], R, "a, Z₀ → AZ₀\na, A → AA\n(push A)", direction="top"
+ ax,
+ states["q₀"],
+ state_r,
+ "a, Z₀ → AZ₀\na, A → AA\n(push A)",
+ LoopStyle(direction="top"),
)
# q₁ self-loop: b, A/ε
- draw_self_loop(ax, *states["q₁"], R, "b, A → ε\n(pop A)", direction="top")
+ draw_self_loop(
+ ax,
+ states["q₁"],
+ state_r,
+ "b, A → ε\n(pop A)",
+ LoopStyle(direction="top"),
+ )
# Key explanation
ax.text(
0.3,
-1.3,
- "Notacja: symbol_wejścia, szczyt_stosu → nowy_szczyt\n"
- "ε = brak symbolu (przejście spontaniczne lub pusty stos)",
+ "Notacja: symbol_wejścia, szczyt_stosu"
+ " → nowy_szczyt\n"
+ "ε = brak symbolu "
+ "(przejście spontaniczne lub pusty stos)",
fontsize=FS_SMALL,
ha="left",
va="center",
- bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
# --- Right: Step trace with stack ---
ax2 = axes[1]
ax2.axis("off")
ax2.set_title(
- 'Ślad wykonania z wizualizacją stosu — wejście: "aabb"',
+ "Ślad wykonania z wizualizacją stosu"
+ ' — wejście: "aabb"',
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
trace_data = [
- ["Krok", "Czytam", "Stan", "Stos (szczyt→)", "Operacja"],
+ [
+ "Krok",
+ "Czytam",
+ "Stan",
+ "Stos (szczyt→)",
+ "Operacja",
+ ],
["start", "—", "q₀", "[Z₀]", "—"],
["1", "a", "q₀", "[A, Z₀]", "push A"],
["2", "a", "q₀", "[A, A, Z₀]", "push A"],
@@ -416,7 +583,7 @@ def draw_pda_recognition() -> None:
loc="center",
bbox=[0.02, 0.08, 0.96, 0.82],
)
- table.auto_set_font_size(False)
+ table.auto_set_font_size(auto=False)
table.set_fontsize(FS)
for (row, _col), cell in table.get_celld().items():
cell.set_edgecolor(GRAY3)
@@ -430,14 +597,20 @@ def draw_pda_recognition() -> None:
ax2.text(
0.5,
0.0,
- 'Wynik: q₂ ∈ F, stos=[Z₀] → "aabb" AKCEPTOWANE ✓\n'
- 'Intuicja: 2x push A (za "aa") + 2x pop A (za "bb") = stos pusty = OK',
+ "Wynik: q₂ ∈ F, stos=[Z₀]"
+ ' → "aabb" AKCEPTOWANE ✓\n'
+ 'Intuicja: 2x push A (za "aa") '
+ '+ 2x pop A (za "bb") = stos pusty = OK',
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
transform=ax2.transAxes,
- bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": LN,
+ },
)
plt.tight_layout()
@@ -448,7 +621,7 @@ def draw_pda_recognition() -> None:
facecolor=BG,
)
plt.close()
- print(" ✓ pda_recognition_example.png")
+ logger.info(" ✓ pda_recognition_example.png")
# ============================================================
@@ -468,16 +641,23 @@ def draw_lba_recognition() -> None:
pad=10,
)
- CELL_W = 0.9
- CELL_H = 0.7
- TAPE_X0 = 1.5
- HEAD_COLOR = "#FFD700"
+ cell_w = 0.9
+ cell_h = 0.7
+ tape_x0 = 1.5
+ head_color = "#FFD700"
- def draw_tape(y, cells, head_pos, label, step_label="") -> None:
- """Draw a tape row with cells, head position highlighted."""
+ def draw_tape(
+ tape_y: float,
+ cells: list[tuple[str, str]],
+ head_pos: int | None,
+ label: str,
+ *,
+ step_label: str = "",
+ ) -> None:
+ """Draw a tape row with cells, head highlighted."""
ax.text(
0.2,
- y + CELL_H / 2,
+ tape_y + cell_h / 2,
label,
ha="right",
va="center",
@@ -485,44 +665,53 @@ def draw_lba_recognition() -> None:
fontweight="bold",
)
for i, (sym, color) in enumerate(cells):
- x = TAPE_X0 + i * CELL_W
- fc = HEAD_COLOR if i == head_pos else color
+ x = tape_x0 + i * cell_w
+ fc = head_color if i == head_pos else color
rect = mpatches.FancyBboxPatch(
- (x, y),
- CELL_W,
- CELL_H,
+ (x, tape_y),
+ cell_w,
+ cell_h,
boxstyle="round,pad=0.03",
lw=1.2,
edgecolor=LN,
facecolor=fc,
)
ax.add_patch(rect)
+ bold = (
+ "bold"
+ if sym in ("X", "Y", "Z")
+ else "normal"
+ )
ax.text(
- x + CELL_W / 2,
- y + CELL_H / 2,
+ x + cell_w / 2,
+ tape_y + cell_h / 2,
sym,
ha="center",
va="center",
fontsize=FS + 2,
- fontweight="bold" if sym in ("X", "Y", "Z") else "normal",
+ fontweight=bold,
family="monospace",
)
if head_pos is not None:
- hx = TAPE_X0 + head_pos * CELL_W + CELL_W / 2
+ hx = (
+ tape_x0
+ + head_pos * cell_w
+ + cell_w / 2
+ )
ax.annotate(
"▼",
- xy=(hx, y + CELL_H),
- xytext=(hx, y + CELL_H + 0.25),
+ xy=(hx, tape_y + cell_h),
+ xytext=(hx, tape_y + cell_h + 0.25),
ha="center",
va="bottom",
- fontsize=8,
+ fontsize=HEAD_MARKER_FONTSIZE,
color="black",
)
if step_label:
- sx = TAPE_X0 + 6 * CELL_W + 0.5
+ sx = tape_x0 + 6 * cell_w + 0.5
ax.text(
sx,
- y + CELL_H / 2,
+ tape_y + cell_h / 2,
step_label,
ha="left",
va="center",
@@ -534,54 +723,84 @@ def draw_lba_recognition() -> None:
},
)
- W = "white"
- MK = GRAY1 # marked cell color
+ white = "white"
+ mk = GRAY1 # marked cell color
# Row 1: Initial tape
- y = 9.0
+ tape_y = 9.0
draw_tape(
- y,
- [("a", W), ("a", W), ("b", W), ("b", W), ("c", W), ("c", W)],
+ tape_y,
+ [
+ ("a", white),
+ ("a", white),
+ ("b", white),
+ ("b", white),
+ ("c", white),
+ ("c", white),
+ ],
0,
"Początek",
- "taśma = [a, a, b, b, c, c], głowica na 0",
+ step_label=(
+ "taśma = [a, a, b, b, c, c], głowica na 0"
+ ),
)
# Row 2: After marking first 'a'
- y = 7.8
+ tape_y = 7.8
draw_tape(
- y,
- [("X", MK), ("a", W), ("b", W), ("b", W), ("c", W), ("c", W)],
+ tape_y,
+ [
+ ("X", mk),
+ ("a", white),
+ ("b", white),
+ ("b", white),
+ ("c", white),
+ ("c", white),
+ ],
1,
"R1, krok 1",
- "zaznacz a→X, szukaj b",
+ step_label="zaznacz a→X, szukaj b",
)
# Row 3: After marking first 'b'
- y = 6.6
+ tape_y = 6.6
draw_tape(
- y,
- [("X", MK), ("a", W), ("Y", MK), ("b", W), ("c", W), ("c", W)],
+ tape_y,
+ [
+ ("X", mk),
+ ("a", white),
+ ("Y", mk),
+ ("b", white),
+ ("c", white),
+ ("c", white),
+ ],
3,
"R1, krok 2",
- "zaznacz b→Y, szukaj c",
+ step_label="zaznacz b→Y, szukaj c",
)
# Row 4: After marking first 'c'
- y = 5.4
+ tape_y = 5.4
draw_tape(
- y,
- [("X", MK), ("a", W), ("Y", MK), ("b", W), ("Z", MK), ("c", W)],
+ tape_y,
+ [
+ ("X", mk),
+ ("a", white),
+ ("Y", mk),
+ ("b", white),
+ ("Z", mk),
+ ("c", white),
+ ],
0,
"R1, krok 3",
- "zaznacz c→Z, wróć na początek",
+ step_label="zaznacz c→Z, wróć na początek",
)
# Runda 2 header
- y = 4.5
+ tape_y = 4.5
ax.text(
- TAPE_X0 + 3 * CELL_W,
- y + 0.3,
+ tape_x0 + 3 * cell_w,
+ tape_y + 0.3,
"═══ RUNDA 2 ═══",
ha="center",
va="center",
@@ -591,53 +810,81 @@ def draw_lba_recognition() -> None:
)
# Row 5: After marking second 'a'
- y = 3.6
+ tape_y = 3.6
draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("b", W), ("Z", MK), ("c", W)],
+ tape_y,
+ [
+ ("X", mk),
+ ("X", mk),
+ ("Y", mk),
+ ("b", white),
+ ("Z", mk),
+ ("c", white),
+ ],
2,
"R2, krok 1",
- "pomiń X, zaznacz a→X, szukaj b",
+ step_label="pomiń X, zaznacz a→X, szukaj b",
)
# Row 6: After marking second 'b'
- y = 2.4
+ tape_y = 2.4
draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("Z", MK), ("c", W)],
+ tape_y,
+ [
+ ("X", mk),
+ ("X", mk),
+ ("Y", mk),
+ ("Y", mk),
+ ("Z", mk),
+ ("c", white),
+ ],
4,
"R2, krok 2",
- "pomiń Y, zaznacz b→Y, szukaj c",
+ step_label="pomiń Y, zaznacz b→Y, szukaj c",
)
# Row 7: After marking second 'c'
- y = 1.2
+ tape_y = 1.2
draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("Z", MK), ("Z", MK)],
+ tape_y,
+ [
+ ("X", mk),
+ ("X", mk),
+ ("Y", mk),
+ ("Y", mk),
+ ("Z", mk),
+ ("Z", mk),
+ ],
None,
"R2, krok 3",
- "zaznacz c→Z, wróć na początek",
+ step_label="zaznacz c→Z, wróć na początek",
)
# Result
- y = 0.0
+ tape_y = 0.0
ax.text(
- TAPE_X0 + 3 * CELL_W,
- y + 0.3,
- 'Wszystko zaznaczone → q_acc → "aabbcc" AKCEPTOWANE ✓',
+ tape_x0 + 3 * cell_w,
+ tape_y + 0.3,
+ "Wszystko zaznaczone → q_acc"
+ ' → "aabbcc" AKCEPTOWANE ✓',
ha="center",
va="center",
fontsize=FS + 1,
fontweight="bold",
- bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": LN,
+ },
)
# Key
ax.text(
- TAPE_X0 + 6 * CELL_W + 0.5,
- y + 0.3,
- 'Ograniczenie LBA:\ngłowica ≤ 6 komórek\n(= |w| = |"aabbcc"|)',
+ tape_x0 + 6 * cell_w + 0.5,
+ tape_y + 0.3,
+ "Ograniczenie LBA:\n"
+ "głowica ≤ 6 komórek\n"
+ '(= |w| = |"aabbcc"|)',
ha="left",
va="center",
fontsize=FS_SMALL,
@@ -656,36 +903,44 @@ def draw_lba_recognition() -> None:
facecolor=BG,
)
plt.close()
- print(" ✓ lba_recognition_example.png")
+ logger.info(" ✓ lba_recognition_example.png")
# ============================================================
# 4. TM Recognition Example — TM for 0ⁿ1ⁿ
# ============================================================
def draw_tm_recognition() -> None:
- """TM tape visualization for 0ⁿ1ⁿ with infinite tape shown."""
+ """TM tape visualization for 0ⁿ1ⁿ with infinite tape."""
_fig, ax = plt.subplots(1, 1, figsize=(11.69, 6.5))
ax.set_xlim(-0.5, 13)
ax.set_ylim(-1, 10.5)
ax.axis("off")
ax.set_title(
"TM — rozpoznawanie 0ⁿ1ⁿ (n=2)\n"
- "Strategia: zaznacz jedno 0 i jedno 1 w każdej rundzie",
+ "Strategia: zaznacz jedno 0 i jedno 1"
+ " w każdej rundzie",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
- CELL_W = 0.9
- CELL_H = 0.7
- TAPE_X0 = 1.5
- HEAD_COLOR = "#FFD700"
+ cell_w = 0.9
+ cell_h = 0.7
+ tape_x0 = 1.5
+ head_color = "#FFD700"
- def draw_tape(y, cells, head_pos, label, step_label="") -> None:
+ def draw_tape(
+ tape_y: float,
+ cells: list[tuple[str, str]],
+ head_pos: int | None,
+ label: str,
+ *,
+ step_label: str = "",
+ ) -> None:
"""Draw tape."""
ax.text(
0.2,
- y + CELL_H / 2,
+ tape_y + cell_h / 2,
label,
ha="right",
va="center",
@@ -693,16 +948,16 @@ def draw_tm_recognition() -> None:
fontweight="bold",
)
for i, (sym, color) in enumerate(cells):
- x = TAPE_X0 + i * CELL_W
- fc = HEAD_COLOR if i == head_pos else color
+ x = tape_x0 + i * cell_w
+ fc = head_color if i == head_pos else color
lw = 1.2
ls = "-"
if sym == "⊔":
ls = "--"
rect = mpatches.FancyBboxPatch(
- (x, y),
- CELL_W,
- CELL_H,
+ (x, tape_y),
+ cell_w,
+ cell_h,
boxstyle="round,pad=0.03",
lw=lw,
edgecolor=LN,
@@ -710,43 +965,51 @@ def draw_tm_recognition() -> None:
linestyle=ls,
)
ax.add_patch(rect)
+ bold = (
+ "bold" if sym in ("X", "Y") else "normal"
+ )
+ clr = GRAY3 if sym == "⊔" else LN
ax.text(
- x + CELL_W / 2,
- y + CELL_H / 2,
+ x + cell_w / 2,
+ tape_y + cell_h / 2,
sym,
ha="center",
va="center",
fontsize=FS + 2,
- fontweight="bold" if sym in ("X", "Y") else "normal",
+ fontweight=bold,
family="monospace",
- color=GRAY3 if sym == "⊔" else LN,
+ color=clr,
)
# ∞ arrow
- last_x = TAPE_X0 + len(cells) * CELL_W
+ last_x = tape_x0 + len(cells) * cell_w
ax.annotate(
"→ ∞",
- xy=(last_x + 0.3, y + CELL_H / 2),
+ xy=(last_x + 0.3, tape_y + cell_h / 2),
fontsize=FS,
ha="left",
va="center",
color=GRAY3,
)
if head_pos is not None:
- hx = TAPE_X0 + head_pos * CELL_W + CELL_W / 2
+ hx = (
+ tape_x0
+ + head_pos * cell_w
+ + cell_w / 2
+ )
ax.annotate(
"▼",
- xy=(hx, y + CELL_H),
- xytext=(hx, y + CELL_H + 0.25),
+ xy=(hx, tape_y + cell_h),
+ xytext=(hx, tape_y + cell_h + 0.25),
ha="center",
va="bottom",
- fontsize=8,
+ fontsize=HEAD_MARKER_FONTSIZE,
color="black",
)
if step_label:
- sx = TAPE_X0 + 8 * CELL_W + 0.8
+ sx = tape_x0 + 8 * cell_w + 0.8
ax.text(
sx,
- y + CELL_H / 2,
+ tape_y + cell_h / 2,
step_label,
ha="left",
va="center",
@@ -758,45 +1021,37 @@ def draw_tm_recognition() -> None:
},
)
- W = "white"
- MK = GRAY1
- BL = "#F0F0F0" # blank cell
+ white = "white"
+ mk = GRAY1
+ bl = "#F0F0F0" # blank cell
- # Row 1: Initial
- y = 9.0
- draw_tape(
- y,
- [("0", W), ("0", W), ("1", W), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- 0,
- "Początek",
- "taśma = [0,0,1,1,⊔,⊔,...∞]",
- )
-
- # Row 2: Mark first 0
- y = 7.8
- draw_tape(
- y,
- [("X", MK), ("0", W), ("1", W), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- 1,
- "R1, krok 1",
- "zaznacz 0→X, idź w prawo",
- )
-
- # Row 3: Skip to first 1, mark it
- y = 6.6
- draw_tape(
- y,
- [("X", MK), ("0", W), ("Y", MK), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- 0,
- "R1, krok 2",
- "zaznacz 1→Y, wróć na początek",
- )
+ tape_rows = [
+ (9.0, [("0", white), ("0", white), ("1", white),
+ ("1", white), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ 0, "Początek", "taśma = [0,0,1,1,⊔,⊔,...∞]"),
+ (7.8, [("X", mk), ("0", white), ("1", white),
+ ("1", white), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ 1, "R1, krok 1", "zaznacz 0→X, idź w prawo"),
+ (6.6, [("X", mk), ("0", white), ("Y", mk),
+ ("1", white), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ 0, "R1, krok 2", "zaznacz 1→Y, wróć na początek"),
+ (4.8, [("X", mk), ("X", mk), ("Y", mk),
+ ("1", white), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ 2, "R2, krok 1", "pomiń X, zaznacz 0→X"),
+ (3.6, [("X", mk), ("X", mk), ("Y", mk),
+ ("Y", mk), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ 0, "R2, krok 2", "pomiń Y, zaznacz 1→Y, wróć"),
+ (2.4, [("X", mk), ("X", mk), ("Y", mk),
+ ("Y", mk), ("⊔", bl), ("⊔", bl), ("⊔", bl)],
+ None, "Sprawdzenie",
+ "brak niezaznaczonych → q_acc"),
+ ]
# Runda 2 header
- y = 5.8
+ runda2_y = 5.8
ax.text(
- TAPE_X0 + 3.5 * CELL_W,
- y + 0.3,
+ tape_x0 + 3.5 * cell_w,
+ runda2_y + 0.3,
"═══ RUNDA 2 ═══",
ha="center",
va="center",
@@ -804,56 +1059,37 @@ def draw_tm_recognition() -> None:
fontweight="bold",
)
- # Row 4: Mark second 0
- y = 4.8
- draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- 2,
- "R2, krok 1",
- "pomiń X, zaznacz 0→X",
- )
-
- # Row 5: Mark second 1
- y = 3.6
- draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- 0,
- "R2, krok 2",
- "pomiń Y, zaznacz 1→Y, wróć",
- )
-
- # Row 6: Check — all marked
- y = 2.4
- draw_tape(
- y,
- [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
- None,
- "Sprawdzenie",
- "brak niezaznaczonych → q_acc",
- )
+ for row_y, cells, head, lbl, step in tape_rows:
+ draw_tape(
+ row_y, cells, head, lbl, step_label=step
+ )
# Result + TM vs LBA comparison
- y = 0.8
+ tape_y = 0.8
ax.text(
- TAPE_X0 + 3.5 * CELL_W,
- y + 0.3,
+ tape_x0 + 3.5 * cell_w,
+ tape_y + 0.3,
'"0011" AKCEPTOWANE ✓',
ha="center",
va="center",
fontsize=FS + 1,
fontweight="bold",
- bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": LN,
+ },
)
- y = -0.3
+ tape_y = -0.3
ax.text(
- TAPE_X0 + 3.5 * CELL_W,
- y + 0.3,
- "Różnica TM vs LBA: taśma TM jest nieskończona (⊔ → ∞)\n"
+ tape_x0 + 3.5 * cell_w,
+ tape_y + 0.3,
+ "Różnica TM vs LBA: taśma TM jest "
+ "nieskończona (⊔ → ∞)\n"
"LBA: głowica ograniczona do |w| komórek\n"
- "TM: głowica może wyjść POZA wejście i pisać na pustych ⊔",
+ "TM: głowica może wyjść POZA wejście "
+ "i pisać na pustych ⊔",
ha="center",
va="center",
fontsize=FS_SMALL,
@@ -872,16 +1108,18 @@ def draw_tm_recognition() -> None:
facecolor=BG,
)
plt.close()
- print(" ✓ tm_recognition_example.png")
+ logger.info(" ✓ tm_recognition_example.png")
# ============================================================
# Main
# ============================================================
if __name__ == "__main__":
- print("Generating automata diagrams for PYTANIE 1...")
+ logger.info(
+ "Generating automata diagrams for PYTANIE 1..."
+ )
draw_fa_recognition()
draw_pda_recognition()
draw_lba_recognition()
draw_tm_recognition()
- print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
+ logger.info("All diagrams saved to %s/", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py
index ccc0e3e..27a554b 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py
@@ -7,6 +7,8 @@ Designed for A4 laser printer output (300 DPI, black & white).
from __future__ import annotations
+import logging
+
import matplotlib as mpl
mpl.use("Agg")
@@ -20,6 +22,8 @@ if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.figure import Figure
+logger = logging.getLogger(__name__)
+
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
@@ -33,19 +37,35 @@ FIXED_COLOR = "#D0F0D0" # light green-ish gray for fixed
FD_ARROW_COLOR = "#444444"
+def _compute_col_widths(
+ headers: list[str],
+ rows: list[list[str]],
+) -> list[float]:
+ """Auto-calculate column widths based on content."""
+ col_widths: list[float] = []
+ for c in range(len(headers)):
+ max_len = len(headers[c])
+ for r in rows:
+ if c < len(r):
+ max_len = max(max_len, len(str(r[c])))
+ col_widths.append(max(max_len * 0.08 + 0.1, 0.5))
+ return col_widths
+
+
def draw_table(
- ax,
- x,
- y,
- title,
- headers,
- rows,
- col_widths=None,
- highlight_cols=None,
- highlight_rows=None,
- highlight_cells=None,
- strikethrough_cells=None,
- title_fontsize=9,
+ ax: Axes,
+ x: float,
+ y: float,
+ title: str,
+ headers: list[str],
+ rows: list[list[str]],
+ *,
+ col_widths: list[float] | None = None,
+ highlight_cols: set[int] | None = None,
+ highlight_rows: set[int] | None = None,
+ highlight_cells: set[tuple[int, int]] | None = None,
+ strikethrough_cells: set[tuple[int, int]] | None = None,
+ title_fontsize: int = 9,
) -> tuple[float, float]:
"""Draw a single table on the axes at position (x, y).
@@ -66,18 +86,10 @@ def draw_table(
Returns:
(width, height) of the drawn table
"""
- n_cols = len(headers)
n_rows = len(rows)
if col_widths is None:
- # Auto-calculate based on content
- col_widths = []
- for c in range(n_cols):
- max_len = len(headers[c])
- for r in rows:
- if c < len(r):
- max_len = max(max_len, len(str(r[c])))
- col_widths.append(max(max_len * 0.08 + 0.1, 0.5))
+ col_widths = _compute_col_widths(headers, rows)
row_height = 0.22
total_width = sum(col_widths)
@@ -172,7 +184,10 @@ def draw_table(
return total_width, total_height + 0.25 # extra for title
-def create_figure(width_inches=11.69, height_inches=8.27) -> tuple[Figure, Axes]:
+def create_figure(
+ width_inches: float = 11.69,
+ height_inches: float = 8.27,
+) -> tuple[Figure, Axes]:
"""Create A4 landscape figure."""
fig, ax = plt.subplots(1, 1, figsize=(width_inches, height_inches), dpi=DPI)
ax.set_xlim(0, width_inches)
@@ -182,7 +197,16 @@ def create_figure(width_inches=11.69, height_inches=8.27) -> tuple[Figure, Axes]
return fig, ax
-def add_arrow(ax, x1, y1, x2, y2, label="", color="black") -> None:
+def add_arrow(
+ ax: Axes,
+ x1: float,
+ y1: float,
+ x2: float,
+ y2: float,
+ label: str = "",
+ *,
+ color: str = "black",
+) -> None:
"""Draw an arrow with optional label."""
ax.annotate(
"",
@@ -205,7 +229,15 @@ def add_arrow(ax, x1, y1, x2, y2, label="", color="black") -> None:
def add_label(
- ax, x, y, text, fontsize=8, color="black", ha="left", style="normal"
+ ax: Axes,
+ x: float,
+ y: float,
+ text: str,
+ *,
+ fontsize: int = 8,
+ color: str = "black",
+ ha: str = "left",
+ style: str = "normal",
) -> None:
"""Add a text label."""
ax.text(
@@ -289,7 +321,10 @@ def draw_0nf() -> None:
ax,
0.8,
1.2,
- "Zaleznosci funkcyjne: StID -> Imie, WydzialID | WydzialID -> NazwaWydzialu",
+ (
+ "Zaleznosci funkcyjne: StID -> Imie, WydzialID"
+ " | WydzialID -> NazwaWydzialu"
+ ),
fontsize=8,
color="#333333",
)
@@ -297,7 +332,10 @@ def draw_0nf() -> None:
ax,
0.8,
0.9,
- " KursID -> NazwaKursu | (StID,KursID) -> Prowadzacy | Prowadzacy -> KursID",
+ (
+ " KursID -> NazwaKursu | (StID,KursID)"
+ " -> Prowadzacy | Prowadzacy -> KursID"
+ ),
fontsize=8,
color="#333333",
)
@@ -309,7 +347,7 @@ def draw_0nf() -> None:
pad_inches=0.2,
)
plt.close(fig)
- print("Generated: nf_0nf_table.png")
+ logger.info("Generated: nf_0nf_table.png")
# ============================================================
@@ -399,7 +437,10 @@ def draw_1nf() -> None:
ax,
0.5,
1.5,
- " Imie, WydzialID, NazwaWydzialu zaleza TYLKO od StID (czesc klucza).",
+ (
+ " Imie, WydzialID, NazwaWydzialu"
+ " zaleza TYLKO od StID (czesc klucza)."
+ ),
fontsize=9,
color="black",
)
@@ -419,7 +460,7 @@ def draw_1nf() -> None:
pad_inches=0.2,
)
plt.close(fig)
- print("Generated: nf_1nf_tables.png")
+ logger.info("Generated: nf_1nf_tables.png")
# ============================================================
@@ -477,7 +518,10 @@ def draw_2nf() -> None:
ax,
0.3,
3.3,
- "KROK: Rozbito czesc. zaleznosci — atrybuty zalezne od czesci klucza wydzielone.",
+ (
+ "KROK: Rozbito czesc. zaleznosci"
+ " — atrybuty zalezne od czesci klucza wydzielone."
+ ),
fontsize=9,
)
add_label(
@@ -528,7 +572,7 @@ def draw_2nf() -> None:
pad_inches=0.2,
)
plt.close(fig)
- print("Generated: nf_2nf_tables.png")
+ logger.info("Generated: nf_2nf_tables.png")
# ============================================================
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
index 3a0f2d8..e7c80e4 100755
--- a/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
@@ -12,18 +12,29 @@
7. Exactly-once.
All: A4-width, B&W, 300 DPI, laser-printer-friendly.
-One diagram per image — no cramming.
+One diagram per image -- no cramming.
"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING
+
import matplotlib as mpl
mpl.use("Agg")
-from pathlib import Path
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+
+logger = logging.getLogger(__name__)
+
DPI = 300
BG = "white"
LN = "black"
@@ -40,108 +51,207 @@ GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
+@dataclass(frozen=True)
+class BoxStyle:
+ """Optional styling for boxes."""
+
+ fill: str = "white"
+ lw: float = 1.2
+ fontsize: float = FS
+ fontweight: str = "normal"
+ ha: str = "center"
+ va: str = "center"
+ rounded: bool = True
+
+
+@dataclass(frozen=True)
+class ArrowCfg:
+ """Config for arrows."""
+
+ lw: float = 1.2
+ style: str = "->"
+ color: str = LN
+ label: str = ""
+ label_offset: float = 0.15
+ label_fs: float = 8
+
+
+@dataclass(frozen=True)
+class DashedCfg:
+ """Config for dashed arrows."""
+
+ lw: float = 1.0
+ color: str = LN
+ label: str = ""
+ label_offset: float = 0.15
+ label_fs: float = 8
+
+
def draw_box(
- ax,
- x,
- y,
- w,
- h,
- text,
- fill="white",
- lw=1.2,
- fontsize=FS,
- fontweight="normal",
- ha="center",
- va="center",
- rounded=True,
+ ax: Axes,
+ pos: tuple[float, float],
+ size: tuple[float, float],
+ text: str,
+ style: BoxStyle | None = None,
) -> None:
"""Draw box."""
- if rounded:
+ s = style or BoxStyle()
+ x, y = pos
+ w, h = size
+ if s.rounded:
rect = FancyBboxPatch(
- (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=s.lw,
+ edgecolor=LN,
+ facecolor=s.fill,
)
else:
- rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ h,
+ lw=s.lw,
+ edgecolor=LN,
+ facecolor=s.fill,
+ )
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
- ha=ha,
- va=va,
- fontsize=fontsize,
- fontweight=fontweight,
+ ha=s.ha,
+ va=s.va,
+ fontsize=s.fontsize,
+ fontweight=s.fontweight,
wrap=True,
)
def draw_arrow(
- ax,
- x1,
- y1,
- x2,
- y2,
- lw=1.2,
- style="->",
- color=LN,
- label="",
- label_offset=0.15,
- label_fs=8,
+ ax: Axes,
+ start: tuple[float, float],
+ end: tuple[float, float],
+ cfg: ArrowCfg | None = None,
) -> None:
"""Draw arrow."""
+ c = cfg or ArrowCfg()
ax.annotate(
"",
- xy=(x2, y2),
- xytext=(x1, y1),
- arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ xy=end,
+ xytext=start,
+ arrowprops={
+ "arrowstyle": c.style,
+ "color": c.color,
+ "lw": c.lw,
+ },
)
- if label:
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
- ax.text(mx, my, label, ha="center", va="bottom", fontsize=label_fs, color=color)
+ if c.label:
+ mx = (start[0] + end[0]) / 2
+ my = (start[1] + end[1]) / 2 + c.label_offset
+ ax.text(
+ mx,
+ my,
+ c.label,
+ ha="center",
+ va="bottom",
+ fontsize=c.label_fs,
+ color=c.color,
+ )
def draw_dashed_arrow(
- ax, x1, y1, x2, y2, lw=1.0, color=LN, label="", label_offset=0.15, label_fs=8
+ ax: Axes,
+ start: tuple[float, float],
+ end: tuple[float, float],
+ cfg: DashedCfg | None = None,
) -> None:
"""Draw dashed arrow."""
+ c = cfg or DashedCfg()
ax.annotate(
"",
- xy=(x2, y2),
- xytext=(x1, y1),
+ xy=end,
+ xytext=start,
arrowprops={
"arrowstyle": "->",
- "color": color,
- "lw": lw,
+ "color": c.color,
+ "lw": c.lw,
"linestyle": "dashed",
},
)
- if label:
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
- ax.text(mx, my, label, ha="center", va="bottom", fontsize=label_fs, color=color)
+ if c.label:
+ mx = (start[0] + end[0]) / 2
+ my = (start[1] + end[1]) / 2 + c.label_offset
+ ax.text(
+ mx,
+ my,
+ c.label,
+ ha="center",
+ va="bottom",
+ fontsize=c.label_fs,
+ color=c.color,
+ )
-def draw_cross(ax, x, y, size=0.15, lw=2.5, color="black") -> None:
+def draw_cross(
+ ax: Axes,
+ pos: tuple[float, float],
+ size: float = 0.15,
+ lw: float = 2.5,
+ color: str = "black",
+) -> None:
"""Draw cross."""
- ax.plot([x - size, x + size], [y - size, y + size], color=color, lw=lw)
- ax.plot([x - size, x + size], [y + size, y - size], color=color, lw=lw)
-
-
-def draw_check(ax, x, y, size=0.15, lw=2.5, color="black") -> None:
- """Draw check."""
- ax.plot([x - size, x - size * 0.2], [y, y - size * 0.7], color=color, lw=lw)
+ x, y = pos
ax.plot(
- [x - size * 0.2, x + size], [y - size * 0.7, y + size * 0.5], color=color, lw=lw
+ [x - size, x + size],
+ [y - size, y + size],
+ color=color,
+ lw=lw,
+ )
+ ax.plot(
+ [x - size, x + size],
+ [y + size, y - size],
+ color=color,
+ lw=lw,
)
-def save(fig, name) -> None:
+def draw_check(
+ ax: Axes,
+ pos: tuple[float, float],
+ size: float = 0.15,
+ lw: float = 2.5,
+ color: str = "black",
+) -> None:
+ """Draw check."""
+ x, y = pos
+ ax.plot(
+ [x - size, x - size * 0.2],
+ [y, y - size * 0.7],
+ color=color,
+ lw=lw,
+ )
+ ax.plot(
+ [x - size * 0.2, x + size],
+ [y - size * 0.7, y + size * 0.5],
+ color=color,
+ lw=lw,
+ )
+
+
+def save(fig: plt.Figure, name: str) -> None:
"""Save."""
plt.tight_layout()
fig.savefig(
- str(Path(OUTPUT_DIR) / name), dpi=DPI, bbox_inches="tight", facecolor=BG
+ str(Path(OUTPUT_DIR) / name),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
)
plt.close(fig)
- print(f" ✓ {name}")
+ logger.info(" \u2713 %s", name)
# ============================================================
@@ -155,84 +265,107 @@ def draw_sub_topic() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- "Subskrypcja topic-based — routing po nazwie tematu",
+ "Subskrypcja topic-based"
+ " \u2014 routing po nazwie tematu",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
- # Publisher + messages
- draw_box(
- ax, 0.2, 3.2, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ bold10 = BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
)
- draw_box(ax, 0.3, 1.8, 2.2, 0.8, 'topic: "orders"', fill=GRAY4, fontsize=8)
- draw_box(ax, 0.3, 0.7, 2.2, 0.8, 'topic: "payments"', fill=GRAY4, fontsize=8)
+ fs85 = BoxStyle(fill=GRAY1, fontsize=8.5)
- # Broker
+ draw_box(
+ ax, (0.2, 3.2), (2.4, 1.1), "Publisher", bold10
+ )
draw_box(
ax,
- 4.2,
- 1.5,
- 2.8,
- 2.2,
+ (0.3, 1.8),
+ (2.2, 0.8),
+ 'topic: "orders"',
+ BoxStyle(fill=GRAY4, fontsize=8),
+ )
+ draw_box(
+ ax,
+ (0.3, 0.7),
+ (2.2, 0.8),
+ 'topic: "payments"',
+ BoxStyle(fill=GRAY4, fontsize=8),
+ )
+
+ draw_box(
+ ax,
+ (4.2, 1.5),
+ (2.8, 2.2),
"BROKER\n\ntopic routing",
- fill=GRAY2,
- fontsize=10,
- fontweight="bold",
+ BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
+ ),
)
- # Subscribers
draw_box(
ax,
- 8.5,
- 3.8,
- 3.0,
- 1.0,
+ (8.5, 3.8),
+ (3.0, 1.0),
'Subscriber A\nsubskrybuje: "orders"',
- fill=GRAY1,
- fontsize=8.5,
+ fs85,
)
draw_box(
ax,
- 8.5,
- 2.2,
- 3.0,
- 1.0,
+ (8.5, 2.2),
+ (3.0, 1.0),
'Subscriber B\nsubskrybuje: "payments"',
- fill=GRAY1,
- fontsize=8.5,
+ fs85,
)
draw_box(
ax,
- 8.5,
- 0.6,
- 3.0,
- 1.0,
+ (8.5, 0.6),
+ (3.0, 1.0),
'Subscriber C\nsubskrybuje: "orders"',
- fill=GRAY1,
- fontsize=8.5,
+ fs85,
)
- # Arrows: publisher → broker
- draw_arrow(ax, 2.6, 2.2, 4.2, 2.8, label_fs=8)
- draw_arrow(ax, 2.6, 1.1, 4.2, 2.2, label_fs=8)
+ fs8 = ArrowCfg(label_fs=8)
+ draw_arrow(ax, (2.6, 2.2), (4.2, 2.8), fs8)
+ draw_arrow(ax, (2.6, 1.1), (4.2, 2.2), fs8)
- # Arrows: broker → subscribers
- draw_arrow(ax, 7.0, 3.4, 8.5, 4.2, label='"orders"', label_fs=8)
- draw_arrow(ax, 7.0, 2.6, 8.5, 2.7, label='"payments"', label_fs=8)
- draw_arrow(ax, 7.0, 2.2, 8.5, 1.2, label='"orders"', label_fs=8)
+ draw_arrow(
+ ax,
+ (7.0, 3.4),
+ (8.5, 4.2),
+ ArrowCfg(label='"orders"', label_fs=8),
+ )
+ draw_arrow(
+ ax,
+ (7.0, 2.6),
+ (8.5, 2.7),
+ ArrowCfg(label='"payments"', label_fs=8),
+ )
+ draw_arrow(
+ ax,
+ (7.0, 2.2),
+ (8.5, 1.2),
+ ArrowCfg(label='"orders"', label_fs=8),
+ )
- # Explanation
ax.text(
6.0,
0.1,
- "Subscriber deklaruje nazwę tematu. Broker kieruje wiadomości\n"
- "do WSZYSTKICH subscriberów danego tematu. Najprostszy model.",
+ "Subscriber deklaruje nazw\u0119 tematu."
+ " Broker kieruje wiadomo\u015bci\n"
+ "do WSZYSTKICH subscriber\u00f3w"
+ " danego tematu. Najprostszy model.",
ha="center",
va="bottom",
fontsize=8.5,
style="italic",
- bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_sub_topic.png")
@@ -249,69 +382,107 @@ def draw_sub_content() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- "Subskrypcja content-based — filtrowanie po treści wiadomości",
+ "Subskrypcja content-based"
+ " \u2014 filtrowanie po tre\u015bci wiadomo\u015bci",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
- # Publisher + message
+ bold10 = BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
+ )
draw_box(
- ax, 0.2, 3.5, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ ax, (0.2, 3.5), (2.4, 1.1), "Publisher", bold10
)
draw_box(
ax,
- 0.2,
- 1.8,
- 2.4,
- 1.2,
+ (0.2, 1.8),
+ (2.4, 1.2),
'price: 150\ntype: "book"\ncategory: "IT"',
- fill=GRAY4,
- fontsize=8.5,
+ BoxStyle(fill=GRAY4, fontsize=8.5),
)
- # Broker
draw_box(
ax,
- 4.0,
- 2.0,
- 3.0,
- 2.5,
- "BROKER\n\newaluuje filtry\nkażdego subscribera",
- fill=GRAY2,
- fontsize=9,
- fontweight="bold",
+ (4.0, 2.0),
+ (3.0, 2.5),
+ "BROKER\n\newaluuje filtry\n"
+ "ka\u017cdego subscribera",
+ BoxStyle(
+ fill=GRAY2, fontsize=9, fontweight="bold"
+ ),
)
- # Subscribers with filters
+ fs9 = BoxStyle(fill=GRAY1, fontsize=9)
draw_box(
- ax, 8.5, 4.2, 3.2, 1.0, "Sub A\nfiltr: price > 100", fill=GRAY1, fontsize=9
+ ax,
+ (8.5, 4.2),
+ (3.2, 1.0),
+ "Sub A\nfiltr: price > 100",
+ fs9,
)
draw_box(
- ax, 8.5, 2.6, 3.2, 1.0, 'Sub B\nfiltr: type = "food"', fill=GRAY1, fontsize=9
+ ax,
+ (8.5, 2.6),
+ (3.2, 1.0),
+ 'Sub B\nfiltr: type = "food"',
+ fs9,
+ )
+ draw_box(
+ ax,
+ (8.5, 1.0),
+ (3.2, 1.0),
+ "Sub C\nfiltr: price < 50",
+ fs9,
)
- draw_box(ax, 8.5, 1.0, 3.2, 1.0, "Sub C\nfiltr: price < 50", fill=GRAY1, fontsize=9)
- # Arrows
- draw_arrow(ax, 2.6, 2.4, 4.0, 3.0)
- draw_arrow(ax, 7.0, 4.0, 8.5, 4.6, label="150 > 100 ✓ dostarczono", label_fs=8)
- draw_dashed_arrow(
- ax, 7.0, 3.2, 8.5, 3.1, label='"book" ≠ "food" ✗ odrzucono', label_fs=8
+ draw_arrow(ax, (2.6, 2.4), (4.0, 3.0))
+ draw_arrow(
+ ax,
+ (7.0, 4.0),
+ (8.5, 4.6),
+ ArrowCfg(
+ label="150 > 100 \u2713 dostarczono",
+ label_fs=8,
+ ),
)
draw_dashed_arrow(
- ax, 7.0, 2.5, 8.5, 1.6, label="150 < 50 ✗ odrzucono", label_fs=8
+ ax,
+ (7.0, 3.2),
+ (8.5, 3.1),
+ DashedCfg(
+ label='"book" \u2260 "food"'
+ " \u2717 odrzucono",
+ label_fs=8,
+ ),
+ )
+ draw_dashed_arrow(
+ ax,
+ (7.0, 2.5),
+ (8.5, 1.6),
+ DashedCfg(
+ label="150 < 50 \u2717 odrzucono",
+ label_fs=8,
+ ),
)
ax.text(
6.0,
0.2,
- "Broker analizuje TREŚĆ wiadomości i ewaluuje predykaty.\n"
- "Bardziej elastyczny niż topic-based, ale wolniejszy (koszt ewaluacji).",
+ "Broker analizuje TRE\u015a\u0106 wiadomo\u015bci"
+ " i ewaluuje predykaty.\n"
+ "Bardziej elastyczny ni\u017c topic-based,"
+ " ale wolniejszy (koszt ewaluacji).",
ha="center",
va="bottom",
fontsize=8.5,
style="italic",
- bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_sub_content.png")
@@ -328,95 +499,153 @@ def draw_sub_type() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- "Subskrypcja type-based — routing po typie (klasie) obiektu",
+ "Subskrypcja type-based"
+ " \u2014 routing po typie (klasie) obiektu",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
- # Publisher
+ bold10 = BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
+ )
draw_box(
- ax, 0.2, 4.2, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ ax, (0.2, 4.2), (2.4, 1.1), "Publisher", bold10
)
- # Messages
- draw_box(ax, 0.1, 2.8, 2.6, 0.9, "new OrderEvent()", fill=GRAY4, fontsize=9)
- draw_box(ax, 0.1, 1.5, 2.6, 0.9, "new PaymentEvent()", fill=GRAY4, fontsize=9)
-
- # Broker
+ fs9_g4 = BoxStyle(fill=GRAY4, fontsize=9)
draw_box(
ax,
- 4.0,
- 2.3,
- 3.0,
- 2.4,
- "BROKER\n\nrouting po\ntypie klasy",
- fill=GRAY2,
- fontsize=10,
- fontweight="bold",
+ (0.1, 2.8),
+ (2.6, 0.9),
+ "new OrderEvent()",
+ fs9_g4,
+ )
+ draw_box(
+ ax,
+ (0.1, 1.5),
+ (2.6, 0.9),
+ "new PaymentEvent()",
+ fs9_g4,
)
- # Subscribers
- draw_box(ax, 8.5, 4.8, 3.2, 1.0, "Sub A\n→ OrderEvent", fill=GRAY1, fontsize=9)
- draw_box(ax, 8.5, 3.2, 3.2, 1.0, "Sub B\n→ PaymentEvent", fill=GRAY1, fontsize=9)
- draw_box(ax, 8.5, 1.6, 3.2, 1.0, "Sub C\n→ Event (base)", fill=GRAY1, fontsize=9)
+ draw_box(
+ ax,
+ (4.0, 2.3),
+ (3.0, 2.4),
+ "BROKER\n\nrouting po\ntypie klasy",
+ BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
+ ),
+ )
- # Arrows
- draw_arrow(ax, 2.7, 3.2, 4.0, 3.8)
- draw_arrow(ax, 2.7, 2.0, 4.0, 3.0)
- draw_arrow(ax, 7.0, 4.3, 8.5, 5.2, label="OrderEvent", label_fs=8)
- draw_arrow(ax, 7.0, 3.5, 8.5, 3.7, label="PaymentEvent", label_fs=8)
- draw_arrow(ax, 7.0, 3.0, 8.5, 2.2, label="oba (dziedziczenie!)", label_fs=8)
+ fs9 = BoxStyle(fill=GRAY1, fontsize=9)
+ draw_box(
+ ax,
+ (8.5, 4.8),
+ (3.2, 1.0),
+ "Sub A\n\u2192 OrderEvent",
+ fs9,
+ )
+ draw_box(
+ ax,
+ (8.5, 3.2),
+ (3.2, 1.0),
+ "Sub B\n\u2192 PaymentEvent",
+ fs9,
+ )
+ draw_box(
+ ax,
+ (8.5, 1.6),
+ (3.2, 1.0),
+ "Sub C\n\u2192 Event (base)",
+ fs9,
+ )
+
+ draw_arrow(ax, (2.7, 3.2), (4.0, 3.8))
+ draw_arrow(ax, (2.7, 2.0), (4.0, 3.0))
+ draw_arrow(
+ ax,
+ (7.0, 4.3),
+ (8.5, 5.2),
+ ArrowCfg(label="OrderEvent", label_fs=8),
+ )
+ draw_arrow(
+ ax,
+ (7.0, 3.5),
+ (8.5, 3.7),
+ ArrowCfg(label="PaymentEvent", label_fs=8),
+ )
+ draw_arrow(
+ ax,
+ (7.0, 3.0),
+ (8.5, 2.2),
+ ArrowCfg(
+ label="oba (dziedziczenie!)", label_fs=8
+ ),
+ )
- # Class hierarchy inset
hx, hy = 0.5, 0.0
draw_box(
ax,
- hx + 2.0,
- hy + 0.2,
- 1.8,
- 0.6,
+ (hx + 2.0, hy + 0.2),
+ (1.8, 0.6),
"Event",
- fill=GRAY3,
- fontsize=8,
- fontweight="bold",
+ BoxStyle(
+ fill=GRAY3, fontsize=8, fontweight="bold"
+ ),
)
- draw_box(ax, hx + 0.0, hy + 0.2, 1.8, 0.6, "OrderEvent", fill=GRAY4, fontsize=7.5)
- draw_box(ax, hx + 4.0, hy + 0.2, 2.0, 0.6, "PaymentEvent", fill=GRAY4, fontsize=7.5)
- draw_arrow(
+ draw_box(
ax,
- hx + 2.9,
- hy + 0.2,
- hx + 0.9,
- hy + 0.2,
- lw=1.0,
- style="->",
- label="extends",
- label_offset=-0.3,
- label_fs=7,
+ (hx, hy + 0.2),
+ (1.8, 0.6),
+ "OrderEvent",
+ BoxStyle(fill=GRAY4, fontsize=7.5),
+ )
+ draw_box(
+ ax,
+ (hx + 4.0, hy + 0.2),
+ (2.0, 0.6),
+ "PaymentEvent",
+ BoxStyle(fill=GRAY4, fontsize=7.5),
)
draw_arrow(
ax,
- hx + 2.9,
- hy + 0.2,
- hx + 5.0,
- hy + 0.2,
- lw=1.0,
- style="->",
- label="extends",
- label_offset=-0.3,
- label_fs=7,
+ (hx + 2.9, hy + 0.2),
+ (hx + 0.9, hy + 0.2),
+ ArrowCfg(
+ lw=1.0,
+ label="extends",
+ label_offset=-0.3,
+ label_fs=7,
+ ),
+ )
+ draw_arrow(
+ ax,
+ (hx + 2.9, hy + 0.2),
+ (hx + 5.0, hy + 0.2),
+ ArrowCfg(
+ lw=1.0,
+ label="extends",
+ label_offset=-0.3,
+ label_fs=7,
+ ),
)
ax.text(
9.5,
0.5,
- "Sub C subskrybuje bazowy Event\n→ otrzymuje WSZYSTKIE podtypy",
+ "Sub C subskrybuje bazowy Event\n"
+ "\u2192 otrzymuje WSZYSTKIE podtypy",
ha="center",
va="center",
fontsize=8.5,
style="italic",
- bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_sub_type.png")
@@ -433,34 +662,58 @@ def draw_sub_hierarchical() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- "Subskrypcja hierarchiczna (wildcards) — wzorce tematów",
+ "Subskrypcja hierarchiczna (wildcards)"
+ " \u2014 wzorce temat\u00f3w",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
- # Topic tree
+ bold10 = BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
+ )
draw_box(
- ax, 4.5, 5.8, 2.4, 0.8, "sensors/", fill=GRAY2, fontsize=10, fontweight="bold"
+ ax, (4.5, 5.8), (2.4, 0.8), "sensors/", bold10
)
- draw_box(ax, 1.5, 4.2, 2.4, 0.8, "temperature/", fill=GRAY3, fontsize=9)
- draw_box(ax, 7.5, 4.2, 2.4, 0.8, "humidity/", fill=GRAY3, fontsize=9)
+ fs9_g3 = BoxStyle(fill=GRAY3, fontsize=9)
+ draw_box(
+ ax,
+ (1.5, 4.2),
+ (2.4, 0.8),
+ "temperature/",
+ fs9_g3,
+ )
+ draw_box(
+ ax,
+ (7.5, 4.2),
+ (2.4, 0.8),
+ "humidity/",
+ fs9_g3,
+ )
- draw_box(ax, 0.2, 2.8, 1.8, 0.7, "room1", fill=GRAY4, fontsize=8.5)
- draw_box(ax, 2.4, 2.8, 1.8, 0.7, "room2", fill=GRAY4, fontsize=8.5)
- draw_box(ax, 6.8, 2.8, 1.8, 0.7, "room1", fill=GRAY4, fontsize=8.5)
- draw_box(ax, 9.0, 2.8, 1.8, 0.7, "room2", fill=GRAY4, fontsize=8.5)
+ fs85_g4 = BoxStyle(fill=GRAY4, fontsize=8.5)
+ draw_box(
+ ax, (0.2, 2.8), (1.8, 0.7), "room1", fs85_g4
+ )
+ draw_box(
+ ax, (2.4, 2.8), (1.8, 0.7), "room2", fs85_g4
+ )
+ draw_box(
+ ax, (6.8, 2.8), (1.8, 0.7), "room1", fs85_g4
+ )
+ draw_box(
+ ax, (9.0, 2.8), (1.8, 0.7), "room2", fs85_g4
+ )
- # Tree edges
- draw_arrow(ax, 5.7, 5.8, 2.7, 5.0, lw=1.0)
- draw_arrow(ax, 5.7, 5.8, 8.7, 5.0, lw=1.0)
- draw_arrow(ax, 2.2, 4.2, 1.1, 3.5, lw=1.0)
- draw_arrow(ax, 3.2, 4.2, 3.3, 3.5, lw=1.0)
- draw_arrow(ax, 8.2, 4.2, 7.7, 3.5, lw=1.0)
- draw_arrow(ax, 9.2, 4.2, 9.9, 3.5, lw=1.0)
+ thin = ArrowCfg(lw=1.0)
+ draw_arrow(ax, (5.7, 5.8), (2.7, 5.0), thin)
+ draw_arrow(ax, (5.7, 5.8), (8.7, 5.0), thin)
+ draw_arrow(ax, (2.2, 4.2), (1.1, 3.5), thin)
+ draw_arrow(ax, (3.2, 4.2), (3.3, 3.5), thin)
+ draw_arrow(ax, (8.2, 4.2), (7.7, 3.5), thin)
+ draw_arrow(ax, (9.2, 4.2), (9.9, 3.5), thin)
- # Full paths
ax.text(
1.1,
2.4,
@@ -480,21 +733,47 @@ def draw_sub_hierarchical() -> None:
style="italic",
)
- # Wildcard examples
ax.text(
- 0.3, 1.5, "Wzorce subskrypcji (MQTT-style):", fontsize=10, fontweight="bold"
+ 0.3,
+ 1.5,
+ "Wzorce subskrypcji (MQTT-style):",
+ fontsize=10,
+ fontweight="bold",
)
patterns = [
- ('"sensors/temperature/room1"', "→ TYLKO room1", "(dokładne dopasowanie)"),
- ('"sensors/temperature/*"', "→ room1, room2", "( * = jeden poziom)"),
- ('"sensors/#"', "→ WSZYSTKO", "( # = dowolna głębokość)"),
+ (
+ '"sensors/temperature/room1"',
+ "\u2192 TYLKO room1",
+ "(dok\u0142adne dopasowanie)",
+ ),
+ (
+ '"sensors/temperature/*"',
+ "\u2192 room1, room2",
+ "( * = jeden poziom)",
+ ),
+ (
+ '"sensors/#"',
+ "\u2192 WSZYSTKO",
+ "( # = dowolna g\u0142\u0119boko\u015b\u0107)",
+ ),
]
for i, (pat, result, note) in enumerate(patterns):
yy = 0.9 - i * 0.55
- ax.text(0.5, yy, pat, fontsize=9, fontweight="bold", fontfamily="monospace")
- ax.text(7.0, yy, result, fontsize=9, fontweight="bold")
- ax.text(9.5, yy, note, fontsize=8, style="italic")
+ ax.text(
+ 0.5,
+ yy,
+ pat,
+ fontsize=9,
+ fontweight="bold",
+ fontfamily="monospace",
+ )
+ ax.text(
+ 7.0, yy, result, fontsize=9, fontweight="bold"
+ )
+ ax.text(
+ 9.5, yy, note, fontsize=8, style="italic"
+ )
save(fig, "pubsub_sub_hierarchical.png")
@@ -510,54 +789,119 @@ def draw_qos_at_most_once() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- 'QoS: At-most-once — „wyślij i zapomnij" (0 lub 1 dostarczenie)',
+ "QoS: At-most-once"
+ " \u2014 \u201ewy\u015blij i zapomnij\u201d"
+ " (0 lub 1 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
- # Actors
px, bx, sx = 1.0, 4.8, 8.5
pw, bw, sw = 2.0, 2.2, 2.0
bh = 0.8
- draw_box(
- ax, px, 5.0, pw, bh, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ bold10_g1 = BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+ bold10_g2 = BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
)
- draw_box(ax, bx, 5.0, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
draw_box(
- ax, sx, 5.0, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ ax, (px, 5.0), (pw, bh), "Publisher", bold10_g1
+ )
+ draw_box(
+ ax, (bx, 5.0), (bw, bh), "Broker", bold10_g2
+ )
+ draw_box(
+ ax,
+ (sx, 5.0),
+ (sw, bh),
+ "Subscriber",
+ bold10_g1,
)
- # Timelines
for xc in [px + pw / 2, bx + bw / 2, sx + sw / 2]:
- ax.plot([xc, xc], [5.0, 1.2], color=GRAY3, lw=1, linestyle=":")
+ ax.plot(
+ [xc, xc],
+ [5.0, 1.2],
+ color=GRAY3,
+ lw=1,
+ linestyle=":",
+ )
# Scenario A: success
y = 4.3
- ax.text(0.2, y + 0.15, "Scenariusz A:", fontsize=8.5, fontweight="bold")
- draw_arrow(ax, px + pw / 2, y, bx + bw / 2, y, label="MSG", label_fs=9)
- draw_arrow(ax, bx + bw / 2, y - 0.6, sx + sw / 2, y - 0.6, label="MSG", label_fs=9)
- draw_check(ax, sx + sw / 2 + 0.4, y - 0.6, size=0.18)
- ax.text(sx + sw / 2 + 0.7, y - 0.6, "OK", fontsize=9, fontweight="bold")
+ ax.text(
+ 0.2,
+ y + 0.15,
+ "Scenariusz A:",
+ fontsize=8.5,
+ fontweight="bold",
+ )
+ msg9 = ArrowCfg(label="MSG", label_fs=9)
+ draw_arrow(
+ ax, (px + pw / 2, y), (bx + bw / 2, y), msg9
+ )
+ draw_arrow(
+ ax,
+ (bx + bw / 2, y - 0.6),
+ (sx + sw / 2, y - 0.6),
+ msg9,
+ )
+ draw_check(ax, (sx + sw / 2 + 0.4, y - 0.6), size=0.18)
+ ax.text(
+ sx + sw / 2 + 0.7,
+ y - 0.6,
+ "OK",
+ fontsize=9,
+ fontweight="bold",
+ )
# Scenario B: lost
y = 2.6
- ax.text(0.2, y + 0.15, "Scenariusz B:", fontsize=8.5, fontweight="bold")
- draw_arrow(ax, px + pw / 2, y, bx + bw / 2, y, label="MSG", label_fs=9)
- draw_dashed_arrow(ax, bx + bw / 2, y - 0.6, 7.5, y - 0.6)
- draw_cross(ax, 7.8, y - 0.6, size=0.2)
- ax.text(8.2, y - 0.55, "UTRACONA", fontsize=9, fontweight="bold")
- ax.text(8.2, y - 1.0, "(brak retransmisji)", fontsize=8, style="italic")
+ ax.text(
+ 0.2,
+ y + 0.15,
+ "Scenariusz B:",
+ fontsize=8.5,
+ fontweight="bold",
+ )
+ draw_arrow(
+ ax, (px + pw / 2, y), (bx + bw / 2, y), msg9
+ )
+ draw_dashed_arrow(
+ ax, (bx + bw / 2, y - 0.6), (7.5, y - 0.6)
+ )
+ draw_cross(ax, (7.8, y - 0.6), size=0.2)
+ ax.text(
+ 8.2,
+ y - 0.55,
+ "UTRACONA",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ 8.2,
+ y - 1.0,
+ "(brak retransmisji)",
+ fontsize=8,
+ style="italic",
+ )
- # Summary
ax.text(
6.0,
0.5,
- "Brak ACK, brak retransmisji. Najszybszy. Use case: logi, metryki, telemetria.",
+ "Brak ACK, brak retransmisji."
+ " Najszybszy. Use case:"
+ " logi, metryki, telemetria.",
ha="center",
va="center",
fontsize=9,
- bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_qos_at_most_once.png")
@@ -574,7 +918,9 @@ def draw_qos_at_least_once() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- 'QoS: At-least-once — „powtarzaj aż potwierdzą" (≥1 dostarczenie)',
+ "QoS: At-least-once"
+ " \u2014 \u201epowtarzaj a\u017c potwierdz\u0105\u201d"
+ " (\u22651 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
@@ -583,70 +929,142 @@ def draw_qos_at_least_once() -> None:
bx, bw = 3.5, 2.2
sx, sw = 8.0, 2.2
bh = 0.8
- draw_box(ax, bx, 5.5, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
draw_box(
- ax, sx, 5.5, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ ax,
+ (bx, 5.5),
+ (bw, bh),
+ "Broker",
+ BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
+ ),
+ )
+ draw_box(
+ ax,
+ (sx, 5.5),
+ (sw, bh),
+ "Subscriber",
+ BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
+ ),
)
- # Timelines
for xc in [bx + bw / 2, sx + sw / 2]:
- ax.plot([xc, xc], [5.5, 0.8], color=GRAY3, lw=1, linestyle=":")
+ ax.plot(
+ [xc, xc],
+ [5.5, 0.8],
+ color=GRAY3,
+ lw=1,
+ linestyle=":",
+ )
# Step 1: send MSG
y1 = 4.8
- draw_arrow(ax, bx + bw / 2, y1, sx + sw / 2, y1, label="MSG #1", label_fs=9)
- draw_check(ax, sx + sw + 0.2, y1, size=0.15)
+ draw_arrow(
+ ax,
+ (bx + bw / 2, y1),
+ (sx + sw / 2, y1),
+ ArrowCfg(label="MSG #1", label_fs=9),
+ )
+ draw_check(ax, (sx + sw + 0.2, y1), size=0.15)
ax.text(sx + sw + 0.5, y1, "odebrano", fontsize=8)
# Step 2: ACK lost
y2 = 3.9
- draw_dashed_arrow(ax, sx + sw / 2, y2, bx + bw + 1.2, y2)
- ax.text((bx + bw / 2 + sx + sw / 2) / 2, y2 + 0.18, "ACK", fontsize=9)
- draw_cross(ax, bx + bw + 0.8, y2, size=0.18)
- ax.text(bx + 0.3, y2 - 0.35, "ACK utracony!", fontsize=8.5, style="italic")
+ draw_dashed_arrow(
+ ax,
+ (sx + sw / 2, y2),
+ (bx + bw + 1.2, y2),
+ )
+ ax.text(
+ (bx + bw / 2 + sx + sw / 2) / 2,
+ y2 + 0.18,
+ "ACK",
+ fontsize=9,
+ )
+ draw_cross(ax, (bx + bw + 0.8, y2), size=0.18)
+ ax.text(
+ bx + 0.3,
+ y2 - 0.35,
+ "ACK utracony!",
+ fontsize=8.5,
+ style="italic",
+ )
- # Step 3: timeout → retry
+ # Step 3: timeout -> retry
y3 = 2.9
ax.text(
- bx + bw / 2, y3 + 0.45, "timeout...", fontsize=8.5, style="italic", ha="center"
+ bx + bw / 2,
+ y3 + 0.45,
+ "timeout...",
+ fontsize=8.5,
+ style="italic",
+ ha="center",
+ )
+ draw_arrow(
+ ax,
+ (bx + bw / 2, y3),
+ (sx + sw / 2, y3),
+ ArrowCfg(label="MSG #1 (retry)", label_fs=9),
+ )
+ draw_check(ax, (sx + sw + 0.2, y3), size=0.15)
+ ax.text(
+ sx + sw + 0.5,
+ y3,
+ "odebrano\n(ponownie!)",
+ fontsize=8,
)
- draw_arrow(ax, bx + bw / 2, y3, sx + sw / 2, y3, label="MSG #1 (retry)", label_fs=9)
- draw_check(ax, sx + sw + 0.2, y3, size=0.15)
- ax.text(sx + sw + 0.5, y3, "odebrano\n(ponownie!)", fontsize=8)
# Step 4: ACK ok
y4 = 2.0
- draw_arrow(ax, sx + sw / 2, y4, bx + bw / 2, y4, label="ACK", label_fs=9)
- draw_check(ax, bx + bw / 2 - 0.5, y4, size=0.18)
+ draw_arrow(
+ ax,
+ (sx + sw / 2, y4),
+ (bx + bw / 2, y4),
+ ArrowCfg(label="ACK", label_fs=9),
+ )
+ draw_check(ax, (bx + bw / 2 - 0.5, y4), size=0.18)
# Duplicate bracket
ax.annotate(
"",
xy=(sx + sw + 1.3, y1),
xytext=(sx + sw + 1.3, y3),
- arrowprops={"arrowstyle": "<->", "color": "black", "lw": 1.2},
+ arrowprops={
+ "arrowstyle": "<->",
+ "color": "black",
+ "lw": 1.2,
+ },
)
ax.text(
sx + sw + 1.6,
(y1 + y3) / 2,
- "DUPLIKAT!\nSubscriber\notrzymał 2x",
+ "DUPLIKAT!\nSubscriber\notrzyma\u0142 2x",
fontsize=9,
ha="left",
va="center",
fontweight="bold",
- bbox={"boxstyle": "round,pad=0.25", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.25",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
- # Summary
ax.text(
6.0,
0.5,
- "Broker czeka na ACK, retransmituje po timeout. Mogą być duplikaty!\n"
- "Use case: zamówienia, płatności (subscriber musi być idempotentny).",
+ "Broker czeka na ACK, retransmituje"
+ " po timeout. Mog\u0105 by\u0107 duplikaty!\n"
+ "Use case: zam\u00f3wienia, p\u0142atno\u015bci"
+ " (subscriber musi by\u0107 idempotentny).",
ha="center",
va="center",
fontsize=9,
- bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_qos_at_least_once.png")
@@ -663,7 +1081,8 @@ def draw_qos_exactly_once() -> None:
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
- "QoS: Exactly-once — 4-krokowy handshake (dokładnie 1 dostarczenie)",
+ "QoS: Exactly-once \u2014 4-krokowy"
+ " handshake (dok\u0142adnie 1 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
@@ -672,30 +1091,65 @@ def draw_qos_exactly_once() -> None:
bx, bw = 2.5, 2.2
sx, sw = 7.5, 2.2
bh = 0.8
- draw_box(ax, bx, 6.0, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
draw_box(
- ax, sx, 6.0, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ ax,
+ (bx, 6.0),
+ (bw, bh),
+ "Broker",
+ BoxStyle(
+ fill=GRAY2, fontsize=10, fontweight="bold"
+ ),
+ )
+ draw_box(
+ ax,
+ (sx, 6.0),
+ (sw, bh),
+ "Subscriber",
+ BoxStyle(
+ fill=GRAY1, fontsize=10, fontweight="bold"
+ ),
)
- # Timelines
for xc in [bx + bw / 2, sx + sw / 2]:
- ax.plot([xc, xc], [6.0, 1.0], color=GRAY3, lw=1, linestyle=":")
+ ax.plot(
+ [xc, xc],
+ [6.0, 1.0],
+ color=GRAY3,
+ lw=1,
+ linestyle=":",
+ )
- # 4-step handshake
steps = [
- (5.2, "right", "PUBLISH (msg_id=42)", "Broker wysyła wiadomość"),
+ (
+ 5.2,
+ "right",
+ "PUBLISH (msg_id=42)",
+ "Broker wysy\u0142a wiadomo\u015b\u0107",
+ ),
(
4.2,
"left",
- "PUBREC (otrzymałem id=42)",
- "Sub potwierdza odbiór, zapisuje id",
+ "PUBREC (otrzyma\u0142em id=42)",
+ "Sub potwierdza odbi\u00f3r,"
+ " zapisuje id",
+ ),
+ (
+ 3.2,
+ "right",
+ "PUBREL (mo\u017cesz przetworzy\u0107)",
+ "Broker zwalnia wiadomo\u015b\u0107",
+ ),
+ (
+ 2.2,
+ "left",
+ "PUBCOMP (zako\u0144czone)",
+ "Sub potwierdza przetworzenie",
),
- (3.2, "right", "PUBREL (możesz przetworzyć)", "Broker zwalnia wiadomość"),
- (2.2, "left", "PUBCOMP (zakończone)", "Sub potwierdza przetworzenie"),
]
- for i, (y, direction, label, desc) in enumerate(steps):
- # Step number
+ for i, (y, direction, label, desc) in enumerate(
+ steps
+ ):
ax.text(
bx + bw / 2 - 0.7,
y,
@@ -704,29 +1158,54 @@ def draw_qos_exactly_once() -> None:
fontweight="bold",
ha="center",
va="center",
- bbox={"boxstyle": "circle,pad=0.18", "facecolor": GRAY3, "edgecolor": LN},
+ bbox={
+ "boxstyle": "circle,pad=0.18",
+ "facecolor": GRAY3,
+ "edgecolor": LN,
+ },
)
if direction == "right":
- draw_arrow(ax, bx + bw / 2, y, sx + sw / 2, y, label=label, label_fs=9)
+ draw_arrow(
+ ax,
+ (bx + bw / 2, y),
+ (sx + sw / 2, y),
+ ArrowCfg(label=label, label_fs=9),
+ )
else:
- draw_arrow(ax, sx + sw / 2, y, bx + bw / 2, y, label=label, label_fs=9)
+ draw_arrow(
+ ax,
+ (sx + sw / 2, y),
+ (bx + bw / 2, y),
+ ArrowCfg(label=label, label_fs=9),
+ )
- # Side description
ax.text(
- sx + sw + 0.3, y, desc, fontsize=8, ha="left", va="center", style="italic"
+ sx + sw + 0.3,
+ y,
+ desc,
+ fontsize=8,
+ ha="left",
+ va="center",
+ style="italic",
)
- # Summary
ax.text(
6.0,
0.6,
- "Deduplikacja po msg_id. Sub nie przetwarza przed PUBREL.\n"
- "Najkosztowniejszy (4 pakiety). Use case: transakcje finansowe, krytyczne zdarzenia.",
+ "Deduplikacja po msg_id."
+ " Sub nie przetwarza przed PUBREL.\n"
+ "Najkosztowniejszy (4 pakiety)."
+ " Use case: transakcje finansowe,"
+ " krytyczne zdarzenia.",
ha="center",
va="center",
fontsize=9,
- bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
)
save(fig, "pubsub_qos_exactly_once.png")
@@ -736,7 +1215,10 @@ def draw_qos_exactly_once() -> None:
# Main
# ============================================================
if __name__ == "__main__":
- print("Generating Pub/Sub diagrams (7 separate images)...")
+ logger.info(
+ "Generating Pub/Sub diagrams"
+ " (7 separate images)..."
+ )
draw_sub_topic()
draw_sub_content()
draw_sub_type()
@@ -744,4 +1226,4 @@ if __name__ == "__main__":
draw_qos_at_most_once()
draw_qos_at_least_once()
draw_qos_exactly_once()
- print("Done!")
+ logger.info("Done!")