"""Implicant module."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import pandas as pd
from qca._constants import DONT_CARE
# ---------------------------------------------------------------------------
# Implicant
# ---------------------------------------------------------------------------
[docs]
@dataclass(frozen=True)
class Implicant:
"""One Quine-McCluskey implicant."""
pattern: tuple[Any, ...]
covered: frozenset[int]
is_prime: bool = True
# ------------------------------------------------------------------
# Analytical properties
# ------------------------------------------------------------------
[docs]
def complexity(self) -> int:
"""Return the number of non-wildcard conditions."""
return sum(1 for v in self.pattern if v != DONT_CARE)
[docs]
def covers(self, minterm_idx: int) -> bool:
"""Return whether this implicant covers a minterm index."""
return minterm_idx in self.covered
[docs]
def subsumes(self, other: Implicant) -> bool:
"""Return whether this implicant logically subsumes another."""
if len(self.pattern) != len(other.pattern):
return False
for sv, ov in zip(self.pattern, other.pattern, strict=False):
# A concrete value in self that differs from other is not subsumed.
if sv == DONT_CARE:
continue
if _is_value_collection(sv):
if not _value_set(ov).issubset(_value_set(sv)):
return False
continue
if sv != ov:
return False
# Coverage must also be contained.
return other.covered.issubset(self.covered)
# ------------------------------------------------------------------
# Label generation
# ------------------------------------------------------------------
[docs]
def label(self, condition_names: list[str]) -> str:
"""Return a human-readable label."""
if len(condition_names) != len(self.pattern):
raise ValueError(
f"condition_names length ({len(condition_names)}) does not match "
f"pattern length ({len(self.pattern)})."
)
parts: list[str] = []
for name, val in zip(condition_names, self.pattern, strict=False):
if val == DONT_CARE:
continue
if _is_value_collection(val):
values = _sorted_values(val)
parts.append(f"{name}={{{','.join(str(v) for v in values)}}}")
continue
if isinstance(val, int):
parts.append(f"~{name}" if val == 0 else name)
else:
parts.append(f"{name}={val}")
return " * ".join(parts) if parts else "(tautology)"
def __repr__(self) -> str:
return (
f"Implicant("
f"pattern={self.pattern}, "
f"covered={set(self.covered)}, "
f"complexity={self.complexity()}"
f")"
)
def _is_value_collection(value: Any) -> bool:
return isinstance(value, (frozenset, tuple)) and not isinstance(value, str)
def _value_set(value: Any) -> frozenset[Any]:
if _is_value_collection(value):
return frozenset(value)
return frozenset([value])
def _sorted_values(values: Any) -> list[Any]:
def _sort_key(value: Any) -> tuple:
try:
return (0, float(value), "")
except (TypeError, ValueError):
return (1, 0.0, str(value))
return sorted(list(values), key=_sort_key)
# ---------------------------------------------------------------------------
# QMSolution
# ---------------------------------------------------------------------------
[docs]
@dataclass
class QMSolution:
"""Complete Quine-McCluskey solution bundle."""
complex_solution: list[Implicant] = field(default_factory=list)
parsimonious_solution: list[Implicant] = field(default_factory=list)
intermediate_solution: list[Implicant] = field(default_factory=list)
prime_implicants_complex: list[Implicant] = field(default_factory=list)
prime_implicants_parsimonious: list[Implicant] = field(default_factory=list)
prime_implicants_intermediate: list[Implicant] = field(default_factory=list)
coverage_table: pd.DataFrame = field(default_factory=pd.DataFrame)
backend: str = "qmc"
condition_names: list[str] = field(default_factory=list)
n_positive: int = 0
n_remainder: int = 0
n_contradiction: int = 0
# ------------------------------------------------------------------
# Aggregation and output
# ------------------------------------------------------------------
[docs]
def summary(self) -> pd.DataFrame:
"""Return a summary DataFrame."""
rows: list[dict] = []
for sol_type, implicants in [
("complex", self.complex_solution),
("parsimonious", self.parsimonious_solution),
("intermediate", self.intermediate_solution),
]:
for imp in implicants:
rows.append(
{
"solution_type": sol_type,
"term": imp.label(self.condition_names),
"complexity": imp.complexity(),
"n_minterms_covered": len(imp.covered),
}
)
if not rows:
return pd.DataFrame(
columns=[
"solution_type",
"term",
"complexity",
"n_minterms_covered",
]
)
order = {"complex": 0, "parsimonious": 1, "intermediate": 2}
df = pd.DataFrame(rows)
df["_order"] = df["solution_type"].map(order)
return (
df.sort_values(["_order", "complexity"])
.drop(columns=["_order"])
.reset_index(drop=True)
)
[docs]
def complexity_reduction(self) -> dict[str, int]:
"""Return each solution's complexity reduction from the complex solution."""
complex_total = sum(imp.complexity() for imp in self.complex_solution)
parsimonious_total = sum(imp.complexity() for imp in self.parsimonious_solution)
intermediate_total = sum(imp.complexity() for imp in self.intermediate_solution)
return {
"parsimonious": complex_total - parsimonious_total,
"intermediate": complex_total - intermediate_total,
}
def __repr__(self) -> str:
return (
f"QMSolution("
f"n_positive={self.n_positive}, "
f"n_remainder={self.n_remainder}, "
f"complex={len(self.complex_solution)} terms, "
f"parsimonious={len(self.parsimonious_solution)} terms, "
f"intermediate={len(self.intermediate_solution)} terms"
f")"
)