Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions nstat/notebook_fidelity_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
IMG_SRC_RE = re.compile(r'<img[^>]+src="([^"]+)"', re.IGNORECASE)
SECTION_RE = re.compile(r"^%%", re.MULTILINE)
PYTHON_SECTION_RE = re.compile(r"^# SECTION\b", re.MULTILINE)
CI_GROUP_ORDER = ("helpfile_full", "parity_core", "ci_smoke", "core", "smoke")


def _repo_root() -> Path:
Expand Down Expand Up @@ -45,6 +46,12 @@ def _count_python_sections(notebook_path: Path) -> int:
return len(PYTHON_SECTION_RE.findall(text))


def _load_notebook_groups(base: Path) -> dict[str, set[str]]:
payload = yaml.safe_load((base / "tools" / "notebooks" / "topic_groups.yml").read_text(encoding="utf-8")) or {}
groups = payload.get("groups", {})
return {str(name): {str(topic) for topic in values or []} for name, values in groups.items()}


def build_notebook_fidelity_audit(
repo_root: Path | None = None,
*,
Expand All @@ -54,6 +61,7 @@ def build_notebook_fidelity_audit(
matlab_root = default_matlab_repo_root(base) if matlab_repo_root is None else matlab_repo_root.resolve()
help_root = matlab_root / "helpfiles"
notes = load_notebook_parity_notes(base)
topic_groups = _load_notebook_groups(base)

items: list[dict[str, Any]] = []
for row in notes:
Expand All @@ -66,12 +74,17 @@ def build_notebook_fidelity_audit(
matlab_m_path = help_root / f"{matlab_stem}.m"
matlab_html_path = help_root / f"{matlab_stem}.html"
matlab_available = matlab_m_path.exists() and matlab_html_path.exists()
current_run_group = next((group for group in CI_GROUP_ORDER if topic in topic_groups.get(group, set())), None)

item: dict[str, Any] = {
"topic": topic,
"source_matlab": str(row["source_matlab"]),
"python_notebook": str(row["file"]),
"status": str(row["fidelity_status"]),
"fidelity_status": str(row["fidelity_status"]),
"executable_in_ci": current_run_group is not None,
"current_run_group": current_run_group,
"fixture_backed": False,
"remaining_differences": str(row["remaining_differences"]),
"python_sections": python_sections,
"python_expected_figures": int(figure_contract.expected_count) if figure_contract else 0,
Expand Down Expand Up @@ -113,6 +126,7 @@ def build_notebook_fidelity_audit(
"matlab": "https://github.com/cajigaslab/nSTAT",
"python": "https://github.com/cajigaslab/nSTAT-python",
},
"status_legend": ["exact", "high_fidelity", "partial", "missing"],
"matlab_repo_root": str(matlab_root),
"items": items,
}
Expand Down
22 changes: 18 additions & 4 deletions nstat/parity_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from nstat.simulink_fidelity import (
iter_outstanding_simulink_items,
load_simulink_fidelity_audit,
normalize_simulink_status,
summarize_simulink_statuses,
summarize_simulink_strategies,
)

Expand Down Expand Up @@ -90,12 +92,12 @@ def render_parity_report(repo_root: Path | None = None) -> str:
notebook_counts = summarize_notebook_fidelity(notebook_fidelity)
notebook_partial = iter_outstanding_notebook_fidelity(notebook_fidelity)
simulink_counts = summarize_simulink_strategies(simulink_fidelity)
simulink_status_counts = summarize_simulink_statuses(simulink_fidelity)
simulink_outstanding = iter_outstanding_simulink_items(simulink_fidelity)
simulink_reference_only = [
row
for row in simulink_fidelity.get("items", [])
if str(row.get("current_python_status", "")).strip() == "reference_only"
or str(row.get("python_strategy", "")).strip() == "reference_only"
if normalize_simulink_status(row) == "reference_only"
]
lines = [
"# nSTAT Python Parity Report",
Expand Down Expand Up @@ -161,6 +163,18 @@ def render_parity_report(repo_root: Path | None = None) -> str:
"",
"## Simulink Fidelity Summary",
"",
"| Status | Count |",
"|---|---:|",
]
)
for status in simulink_fidelity.get("status_legend", ()):
lines.append(f"| `{status}` | {simulink_status_counts.get(status, 0)} |")

lines.extend(
[
"",
"## Simulink Strategy Summary",
"",
"| Strategy | Count |",
"|---|---:|",
]
Expand Down Expand Up @@ -277,11 +291,11 @@ def render_parity_report(repo_root: Path | None = None) -> str:
else:
for row in simulink_outstanding:
lines.append(
f"- `{row['model_name']}` -> `{row['model_path']}` [{row['python_strategy']}/{row['current_python_status']}]: {row['chosen_interoperability_strategy']}"
f"- `{row['model_name']}` -> `{row['model_path']}` [{normalize_simulink_status(row)}]: {row['chosen_interoperability_strategy']}"
)
for row in simulink_reference_only:
lines.append(
f"- `{row['model_name']}` -> `{row['model_path']}` [{row['python_strategy']}/{row['current_python_status']}]: {row['chosen_interoperability_strategy']}"
f"- `{row['model_name']}` -> `{row['model_path']}` [{normalize_simulink_status(row)}]: {row['chosen_interoperability_strategy']}"
)

lines.extend(["", "## Justified Non-Applicable Items", ""])
Expand Down
50 changes: 49 additions & 1 deletion nstat/simulink_fidelity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
import yaml


STATUS_LEGEND = (
"exact_native_python",
"high_fidelity_native_python",
"generated_code_wrapped",
"packaged_runtime",
"matlab_engine_reference",
"reference_only",
"unsupported",
)


def _repo_root() -> Path:
return Path(__file__).resolve().parents[1]

Expand All @@ -16,6 +27,30 @@ def load_simulink_fidelity_audit(repo_root: Path | None = None) -> dict[str, Any
return yaml.safe_load(path.read_text(encoding="utf-8"))


def normalize_simulink_status(row: dict[str, Any]) -> str:
explicit = str(row.get("status", "")).strip()
if explicit:
return explicit

strategy = str(row.get("python_strategy", "")).strip()
current = str(row.get("current_python_status", "")).strip()
if strategy == "reference_only" or current == "reference_only":
return "reference_only"
if strategy == "generated_code_wrapped":
return "generated_code_wrapped"
if strategy == "packaged_runtime":
return "packaged_runtime"
if strategy == "matlab_engine_fallback":
return "matlab_engine_reference"
if strategy == "unsupported" or current == "unsupported":
return "unsupported"
if strategy == "native_python" and current == "exact":
return "exact_native_python"
if strategy == "native_python" and current == "high_fidelity":
return "high_fidelity_native_python"
return current or strategy or "unsupported"


def summarize_simulink_strategies(payload: dict[str, Any]) -> dict[str, int]:
counts = {status: 0 for status in payload.get("strategy_legend", [])}
for row in payload.get("items", []):
Expand All @@ -26,16 +61,29 @@ def summarize_simulink_strategies(payload: dict[str, Any]) -> dict[str, int]:
return counts


def summarize_simulink_statuses(payload: dict[str, Any]) -> dict[str, int]:
counts = {status: 0 for status in payload.get("status_legend", STATUS_LEGEND)}
for row in payload.get("items", []):
status = normalize_simulink_status(row)
if status not in counts:
counts[status] = 0
counts[status] += 1
return counts


def iter_outstanding_simulink_items(payload: dict[str, Any]) -> list[dict[str, Any]]:
return [
row
for row in payload.get("items", [])
if row.get("current_python_status") in {"missing", "partial", "unsupported"}
if normalize_simulink_status(row) in {"unsupported"}
]


__all__ = [
"iter_outstanding_simulink_items",
"load_simulink_fidelity_audit",
"normalize_simulink_status",
"STATUS_LEGEND",
"summarize_simulink_statuses",
"summarize_simulink_strategies",
]
Loading