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
110 changes: 110 additions & 0 deletions chipcompiler/analysis/qor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""ECC-QoR draft 3 analysis engine.

Single source of truth for QoR scoring: build a workspace-level
analysis from the per-step analysis artifacts, emit the versioned
``home/qor_report.json`` contract, and render the CLI text report.
The GUI is a renderer of this report, not a second scorer.
"""

from datetime import UTC, datetime
from pathlib import Path

from chipcompiler.analysis.qor import schema as qor_schema
from chipcompiler.analysis.qor.diagnosis import build_diagnoses
from chipcompiler.analysis.qor.dimensions import evaluate_dimensions
from chipcompiler.analysis.qor.evidence import evaluate_evidence
from chipcompiler.analysis.qor.feasibility import evaluate_feasibility
from chipcompiler.analysis.qor.features import compute_features
from chipcompiler.analysis.qor.loader import load_workspace_qor_inputs
from chipcompiler.analysis.qor.models import (
SCHEMA_VERSION,
SCORING_ENGINE,
InflationView,
PowerObservation,
QorAnalysis,
)
from chipcompiler.analysis.qor.scoring import evaluate_scalar_summary
from chipcompiler.utility.json import json_write

REPORT_FILENAME = "qor_report.json"


def build_qor_analysis(workspace) -> QorAnalysis:
"""Analyze one workspace's current analysis outputs (spec §14.2 facade)."""
inputs = load_workspace_qor_inputs(workspace)
bundle = compute_features(inputs)
dimensions = evaluate_dimensions(bundle, inputs)
feasibility = evaluate_feasibility(inputs)
evidence = evaluate_evidence(inputs, bundle)
scalar = evaluate_scalar_summary(feasibility, dimensions, inputs.profile)
diagnoses = build_diagnoses(feasibility, dimensions, bundle, inputs)

return QorAnalysis(
schema_version=SCHEMA_VERSION,
scoring_engine=SCORING_ENGINE,
design=inputs.design,
workspace=inputs.workspace_path,
timestamp=datetime.now(UTC).isoformat(),
profile=inputs.profile,
tclk_ns=inputs.tclk_ns,
feasibility=feasibility,
evidence=evidence,
qor_record=dimensions,
scalar_summary=scalar,
diagnoses=diagnoses,
inflation=InflationView(
i_place=bundle.i_place,
i_route=bundle.i_route,
i_total=bundle.i_total,
congestion_severity=bundle.congestion_severity,
compatibility_status=(
bundle.compatibility.status if bundle.compatibility else "INCOMPATIBLE"
),
),
power=PowerObservation(
total_uw=inputs.power_total_uw,
budget_uw=inputs.power_budget_uw,
source_path=inputs.power_source_path,
source_kind=inputs.power_source_kind,
corner=inputs.power_corner,
),
flow_steps=dict(inputs.flow_states),
config_warnings=list(inputs.config_warnings),
)


def report_path(workspace) -> Path:
return Path(workspace.directory or "") / "home" / REPORT_FILENAME


def write_qor_report(workspace, analysis=None) -> Path:
"""Persist the versioned report consumed by ECOS Studio (hard cut)."""
analysis = analysis if analysis is not None else build_qor_analysis(workspace)
payload = analysis.to_dict()
violations = qor_schema.validate_report(payload)
if violations:
raise ValueError(f"qor report failed schema validation: {violations}")
destination = report_path(workspace)
if not json_write(file_path=destination, data=payload):
raise OSError(f"failed to write qor report: {destination}")
return destination


def refresh_workspace_qor_report(workspace) -> Path:
"""Post-step hook entry point; callers own exception handling."""
return write_qor_report(workspace)


def render_qor_analysis(analysis, inputs=None) -> str:
from chipcompiler.analysis.qor.renderer import render

return render(analysis, inputs)


__all__ = [
"build_qor_analysis",
"render_qor_analysis",
"refresh_workspace_qor_report",
"report_path",
"write_qor_report",
]
100 changes: 100 additions & 0 deletions chipcompiler/analysis/qor/calibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Calibration functions and default threshold constants (spec §3.2, §8).

Two mathematical classes exist and are never interchanged:

* ``psi_cost`` — one-sided monotone non-increasing cost calibration for
lower-is-better quantities that have a preferred plateau (interconnect
inflation, power budget consumption, timing violation scale). Values at
or below the preferred threshold receive full credit; approaching the
geometric lower bound is never penalized.
* ``psi_target`` — two-sided target-interval calibration for parameters
where both under- and over-allocation are sub-optimal (placed core
utilization). ``tau_min > 0`` keeps the under-allocation branch
well-defined, so metrics whose preferred lower bound is zero-adjacent
(inflation >= 1.0) must use ``psi_cost`` instead.

Default thresholds are CALIBRATED_HEURISTIC values from the spec. They
are engineering defaults, not physical laws; every consumer must present
them as calibrated policy.
"""

from math import isfinite


class CalibrationError(ValueError):
"""Raised when calibration parameters violate their ordering contract."""


def _finite(value) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value)


def psi_cost(value: float, tau_pref: float, tau_fail: float) -> float:
"""One-sided monotone cost calibration (spec eq. 10)."""
if not (_finite(tau_pref) and _finite(tau_fail)) or not tau_pref < tau_fail:
raise CalibrationError(f"require tau_pref < tau_fail, got {tau_pref!r}, {tau_fail!r}")
if not _finite(value):
raise CalibrationError(f"non-finite calibration input: {value!r}")
if value <= tau_pref:
return 1.0
if value >= tau_fail:
return 0.0
return (tau_fail - value) / (tau_fail - tau_pref)


def psi_target(value: float, tau_min: float, tau_max: float, tau_fail: float) -> float:
"""Two-sided target-interval calibration (spec eq. 11)."""
if not (_finite(tau_min) and _finite(tau_max) and _finite(tau_fail)):
raise CalibrationError("target-interval thresholds must be finite numbers")
if not 0 < tau_min <= tau_max < tau_fail:
raise CalibrationError(
f"require 0 < tau_min <= tau_max < tau_fail, got {tau_min!r}, {tau_max!r}, {tau_fail!r}"
)
if not _finite(value):
raise CalibrationError(f"non-finite calibration input: {value!r}")
if tau_min <= value <= tau_max:
return 1.0
if value < tau_min:
return value / tau_min
return max(0.0, min(1.0, (tau_fail - value) / (tau_fail - tau_max)))


def clamp01(value: float) -> float:
if not _finite(value):
raise CalibrationError(f"non-finite clamp input: {value!r}")
return max(0.0, min(1.0, value))


# --- Default calibration constants (spec §8) ---------------------------------

# Interconnect inflation (I_total, and I_place under the D-C ladder).
TAU_I_PREF = 1.25
TAU_I_FAIL = 1.75

# Placed core utilization target interval.
TAU_AREA_MIN = 0.45
TAU_AREA_MAX = 0.70
TAU_AREA_FAIL = 0.85

# Robustness: structural clock imbalance vs multi-corner PVT dispersion.
W_CTS_IMBALANCE = 0.5
W_PVT_DISPERSION = 0.5

# Congestion severity normalization (spec eq. 21).
TAU_RUDY_FAIL = 1.0
TAU_EGR_MAX_FAIL = 20.0
TAU_EGR_TOTAL_FAIL = 100.0

# Timing fractions of Tclk (USER_PROJECT_CONSTRAINT / CALIBRATED_HEURISTIC).
GUARDBAND_FRACTION = 0.05
TIMING_FAIL_FRACTION = 0.20
OVER_PROVISION_FRACTION = 0.20

# Signoff-gate severity normalization references (spec eq. 52-54).
TAU_DRC_REF = 100.0
TAU_LVS_REF = 50.0
HARDEN_DELIVERABLE_COUNT = 3 # GDS, LEF, LIB

# Feature watch levels for robustness contributors (POLICY threshold):
# a contributor consuming less than 10% of its dimension budget is clean.
ROBUSTNESS_CONTRIBUTOR_WATCH = 0.10
57 changes: 57 additions & 0 deletions chipcompiler/analysis/qor/compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Cross-stage net population compatibility (spec §4.3, degraded per D-C).

Full ``MAPPED_COMPATIBLE`` verification needs a NetMapping contract that
the toolchain does not emit yet: CTS inserts clock-tree nets and neither
``place.map.json`` nor the route DB carries per-net data. Until such an
artifact exists the route side is conservatively INCOMPATIBLE and the
route-referencing ratios evaluate strictly to UNKNOWN; the same-stage
place ratios (one netlist, EXACT population) stay computable. QI then
degrades to the ``I_place`` ladder with downgraded evidence instead of
pretending a reconciled cross-stage identity.
"""

from dataclasses import dataclass, field

EXACT_COMPATIBLE = "EXACT_COMPATIBLE"
MAPPED_COMPATIBLE = "MAPPED_COMPATIBLE"
INCOMPATIBLE = "INCOMPATIBLE"


@dataclass(frozen=True)
class Compatibility:
status: str
assumptions: str = ""
net_mapping: dict = field(default_factory=dict)

def to_contract(self) -> dict:
contract = {"status": self.status}
if self.assumptions:
contract["assumptions"] = self.assumptions
if self.net_mapping:
contract["net_mapping"] = dict(self.net_mapping)
return contract


def evaluate_route_compatibility(*, cts_transformed: bool) -> Compatibility:
"""Classify the place→route population used by route-side ratios.

``cts_transformed`` is True when the CTS step completed and inserted
physical clock-tree buffering (or its counts are unknown, which is
treated conservatively as transformed).
"""
if cts_transformed:
return Compatibility(
status=INCOMPATIBLE,
assumptions=(
"CTS inserted clock-tree nets and no verified net mapping "
"exists yet, so route populations cannot be reconciled with "
"placement; route-referencing inflation ratios stay UNKNOWN."
),
)
return Compatibility(
status=EXACT_COMPATIBLE,
assumptions=(
"No CTS transformation is present; route and placement nets "
"share one population (identity mapping)."
),
)
Loading
Loading