Partition metadata structures and planner for Paley graph factorization.
"""Partition metadata structures and planner for Paley graph factorization."""
from __future__ import annotations
from dataclasses import dataclass, field
from statistics import fmean
from typing import Any, Dict, Iterable, List, Mapping, Sequence, Tuple
import networkx as nx
import numpy as np
try:
from tnfr.metrics.common import compute_coherence
from tnfr.metrics.sense_index import compute_Si
HAS_PARTITION_METRICS = True
except ImportError: # pragma: no cover - optional dependency in minimal installs
HAS_PARTITION_METRICS = False
compute_coherence = None # type: ignore
compute_Si = None # type: ignore
@dataclass
class PaleyPartitionTelemetry:
"""Telemetry snapshot collected for a single partition.
Fields include structural field tetrad proxies and pressure decomposition
per §6-7 of TNFR_NUMBER_THEORY.md.
"""
phi_s: float = 0.0
phase_gradient: float = 0.0
phase_curvature: float = 0.0
coherence_length: float = 0.0
coherence: float = 0.0
sense_index: float = 0.0
# Pressure component breakdown (§6, TNFR_NUMBER_THEORY.md)
pressure_components: Dict[str, float] = field(default_factory=dict)
notes: str = ""
def _mean_from_iter(values: Iterable[float]) -> float:
data = [float(v) for v in values if isinstance(v, (int, float))]
if not data:
return 0.0
try:
return float(fmean(data))
except Exception: # pragma: no cover - fmean fallback
return float(sum(data) / len(data))
def _measure_partition_health(
graph: nx.Graph, nodes: Sequence[int]
) -> Tuple[float, float]:
if not nodes or graph is None or not HAS_PARTITION_METRICS:
return 0.0, 0.0
subgraph = graph.subgraph(nodes).copy()
coherence_value = 0.0
sense_value = 0.0
if compute_coherence:
try:
coherence_value = float(compute_coherence(subgraph))
except Exception: # pragma: no cover - telemetry fallback
coherence_value = 0.0
if compute_Si:
try:
si_payload = compute_Si(subgraph, inplace=False)
if isinstance(si_payload, dict):
sense_value = _mean_from_iter(si_payload.values())
else:
arr = np.asarray(si_payload, dtype=float)
sense_value = float(arr.mean()) if arr.size else 0.0
except Exception: # pragma: no cover - telemetry fallback
sense_value = 0.0
return coherence_value, sense_value
@dataclass
class PaleyPartition:
"""Describes a partition of the Paley graph used during factorization."""
partition_id: str
node_indices: List[int]
boundary_nodes: List[int] = field(default_factory=list)
phase_reference: float = 0.0
telemetry: PaleyPartitionTelemetry | None = None
metadata: Dict[str, Any] = field(default_factory=dict)
candidate_factors: List[int] = field(default_factory=list)
def to_mapping(self) -> Dict[str, Any]:
"""Serialize the partition for certificates or telemetry."""
payload: Dict[str, Any] = {
"id": self.partition_id,
"node_indices": list(self.node_indices),
"boundary_nodes": list(self.boundary_nodes),
"phase_reference": self.phase_reference,
}
if self.telemetry is not None:
payload["telemetry"] = vars(self.telemetry)
if self.metadata:
payload["metadata"] = dict(self.metadata)
if self.candidate_factors:
payload["candidate_factors"] = list(self.candidate_factors)
return payload
@dataclass
class PartitionedPaleyGraph:
"""Container describing the partitioning layout for a Paley graph."""
modulus: int
partitions: List[PaleyPartition]
overlap_policy: str = "phase_reference"
notes: str = ""
def iter_partitions(self) -> Iterable[PaleyPartition]:
"""Iterate over partitions in deterministic order."""
yield from self.partitions
def summary(self) -> Mapping[str, Any]:
"""Return a summary suitable for certificates or logging."""
return {
"modulus": self.modulus,
"partition_count": len(self.partitions),
"overlap_policy": self.overlap_policy,
"notes": self.notes,
}
@classmethod
def single_partition(
cls, modulus: int, graph: nx.Graph | None = None
) -> PartitionedPaleyGraph:
"""Convenience helper that models the entire graph as one partition."""
nodes = list(graph.nodes()) if graph is not None else []
partition = PaleyPartition(partition_id="all", node_indices=nodes)
return cls(modulus=modulus, partitions=[partition])
@dataclass
class PartitionPlannerConfig:
"""Configuration parameters for the Paley partition planner."""
target_size: int = 256
boundary_overlap: int = 4
notes: str = "auto"
def plan_paley_partitions(
graph: nx.Graph,
modulus: int,
*,
phi_s: float,
phase_gradient: float,
phase_curvature: float,
coherence_length: float,
config: PartitionPlannerConfig | None = None,
) -> PartitionedPaleyGraph:
"""Compute a simple deterministic partitioning of the Paley graph."""
if graph.number_of_nodes() == 0:
return PartitionedPaleyGraph.single_partition(modulus, graph)
cfg = config or PartitionPlannerConfig()
target_size = max(1, cfg.target_size)
overlap = max(0, cfg.boundary_overlap)
nodes = sorted(graph.nodes())
total_nodes = len(nodes)
step = max(1, target_size - overlap)
partitions: List[PaleyPartition] = []
for idx, start in enumerate(range(0, total_nodes, step)):
chunk = nodes[start : start + target_size]
chunk_set = set(chunk)
boundary_nodes = [
node
for node in chunk
if any(neigh not in chunk_set for neigh in graph.neighbors(node))
]
ratio = len(chunk) / total_nodes if total_nodes else 0.0
coherence_value, sense_value = _measure_partition_health(graph, chunk)
telemetry = PaleyPartitionTelemetry(
phi_s=phi_s * ratio,
phase_gradient=phase_gradient,
phase_curvature=phase_curvature,
coherence_length=coherence_length * ratio,
coherence=coherence_value,
sense_index=sense_value,
notes=f"planner={cfg.notes}",
)
partition = PaleyPartition(
partition_id=f"p{idx}",
node_indices=chunk,
boundary_nodes=boundary_nodes,
phase_reference=0.0,
telemetry=telemetry,
metadata={
"range": [chunk[0], chunk[-1]] if chunk else [],
"ratio": ratio,
"size": len(chunk),
},
)
partitions.append(partition)
return PartitionedPaleyGraph(
modulus=modulus,
partitions=partitions,
overlap_policy="boundary_overlap",
notes=f"target={target_size}",
)
@dataclass
class PartitionAggregation:
"""Aggregated telemetry derived from all partitions."""
partition_count: int
node_total: int
boundary_fraction: float
phi_s_weighted: float
phi_s_ratio: float
phase_gradient_max: float
phase_gradient_ratio: float
phase_curvature_max: float
phase_curvature_ratio: float
coherence_length_weighted: float
coherence_ratio: float
coverage_ratio: float
candidate_total: int
candidate_ratio: float
partition_candidates: Dict[str, List[int]]
empty_partitions: List[str]
notes: str = ""
def to_mapping(self) -> Dict[str, Any]:
return {
"partition_count": self.partition_count,
"node_total": self.node_total,
"boundary_fraction": self.boundary_fraction,
"phi_s_weighted": self.phi_s_weighted,
"phi_s_ratio": self.phi_s_ratio,
"phase_gradient_max": self.phase_gradient_max,
"phase_gradient_ratio": self.phase_gradient_ratio,
"phase_curvature_max": self.phase_curvature_max,
"phase_curvature_ratio": self.phase_curvature_ratio,
"coherence_length_weighted": self.coherence_length_weighted,
"coherence_ratio": self.coherence_ratio,
"coverage_ratio": self.coverage_ratio,
"candidate_total": self.candidate_total,
"candidate_ratio": self.candidate_ratio,
"partition_candidates": self.partition_candidates,
"empty_partitions": self.empty_partitions,
"notes": self.notes,
}
def aggregate_partition_metrics(
partitioned: PartitionedPaleyGraph,
*,
parent_phi_s: float,
parent_phase_gradient: float,
parent_phase_curvature: float,
parent_coherence_length: float,
total_candidate_count: int,
) -> PartitionAggregation:
"""Combine per-partition telemetry into global aggregates."""
partitions = list(partitioned.iter_partitions())
if not partitions:
return PartitionAggregation(
partition_count=0,
node_total=0,
boundary_fraction=0.0,
phi_s_weighted=0.0,
phi_s_ratio=0.0,
phase_gradient_max=0.0,
phase_gradient_ratio=0.0,
phase_curvature_max=0.0,
phase_curvature_ratio=0.0,
coherence_length_weighted=0.0,
coherence_ratio=0.0,
coverage_ratio=0.0,
candidate_total=0,
candidate_ratio=0.0,
partition_candidates={},
empty_partitions=[],
notes="no-partitions",
)
node_total = sum(len(part.node_indices) for part in partitions)
unique_nodes = len({node for part in partitions for node in part.node_indices})
boundary_nodes = {node for part in partitions for node in part.boundary_nodes}
phi_sum = 0.0
coherence_sum = 0.0
gradient_max = 0.0
curvature_max = 0.0
candidate_total = 0
partition_candidates: Dict[str, List[int]] = {}
empty_partitions: List[str] = []
for part in partitions:
telemetry = part.telemetry
if telemetry is not None:
phi_sum += telemetry.phi_s
coherence_sum += telemetry.coherence_length
gradient_max = max(gradient_max, telemetry.phase_gradient)
curvature_max = max(curvature_max, telemetry.phase_curvature)
metadata_candidates = list(part.metadata.get("candidate_factors", []))
candidate_list = list(part.candidate_factors)
combined = metadata_candidates + candidate_list
if combined:
deduped = sorted(set(combined))
partition_candidates[part.partition_id] = deduped
candidate_total += len(deduped)
else:
partition_candidates[part.partition_id] = []
empty_partitions.append(part.partition_id)
phi_ratio = (phi_sum / parent_phi_s) if parent_phi_s else 0.0
coherence_ratio = (
(coherence_sum / parent_coherence_length) if parent_coherence_length else 0.0
)
gradient_ratio = (
(gradient_max / parent_phase_gradient) if parent_phase_gradient else 0.0
)
curvature_ratio = (
(curvature_max / parent_phase_curvature) if parent_phase_curvature else 0.0
)
coverage_ratio = unique_nodes / partitioned.modulus if partitioned.modulus else 0.0
boundary_fraction = (len(boundary_nodes) / unique_nodes) if unique_nodes else 0.0
candidate_ratio = (
(candidate_total / total_candidate_count) if total_candidate_count else 0.0
)
return PartitionAggregation(
partition_count=len(partitions),
node_total=node_total,
boundary_fraction=boundary_fraction,
phi_s_weighted=phi_sum,
phi_s_ratio=phi_ratio,
phase_gradient_max=gradient_max,
phase_gradient_ratio=gradient_ratio,
phase_curvature_max=curvature_max,
phase_curvature_ratio=curvature_ratio,
coherence_length_weighted=coherence_sum,
coherence_ratio=coherence_ratio,
coverage_ratio=coverage_ratio,
candidate_total=candidate_total,
candidate_ratio=candidate_ratio,
partition_candidates=partition_candidates,
empty_partitions=empty_partitions,
notes="aggregated",
)
def annotate_partition_candidates(
partitioned: PartitionedPaleyGraph,
candidates: Sequence[int],
) -> Dict[str, List[int]]:
"""Distribute candidate factors across partitions deterministically."""
partitions = list(partitioned.iter_partitions())
if not partitions or not candidates:
return {part.partition_id: [] for part in partitions}
assignment: Dict[str, List[int]] = {part.partition_id: [] for part in partitions}
for idx, candidate in enumerate(candidates):
target = partitions[idx % len(partitions)]
target.candidate_factors.append(candidate)
assignment[target.partition_id].append(candidate)
target.metadata.setdefault("candidate_factors", []).append(candidate)
return assignment