core/sensorium/vision/operators.py

100 lines
4 KiB
Python

"""Operator registry and rotor lowering for vision_core_v1."""
from __future__ import annotations
import math
from dataclasses import dataclass, field
import numpy as np
from sensorium.vision.checksum import sha256_json
from sensorium.vision.types import VisualEvent
CL41_DIM = 32
ELLIPTIC_PLANES: tuple[int, ...] = (6, 7, 8, 10, 11, 13)
THETA_STEP = math.pi / 512.0
@dataclass(frozen=True, slots=True)
class VisionOperatorSpec:
operator_id: str
event_type: str
blade_alias: str
blade_index: int
base_theta_q: int
gain_rules: tuple[tuple[str, int], ...]
theta_clip_q: int
version: str = "1"
def __post_init__(self) -> None:
if self.blade_index not in ELLIPTIC_PLANES:
raise ValueError(
f"operator '{self.operator_id}' uses non-elliptic blade "
f"{self.blade_index}; v1 permits only {ELLIPTIC_PLANES}"
)
def theta_q_from_event(self, event: VisualEvent) -> int:
attrs = dict(event.attrs)
theta_q = self.base_theta_q
# Position modulates theta in v1; it is not a separate CGA generator.
theta_q += 2 * event.coord.scale_level + event.coord.tile_row + event.coord.tile_col
for attr_name, gain in self.gain_rules:
value = attrs.get(attr_name, 0)
if isinstance(value, int):
theta_q += gain * value
return max(0, min(self.theta_clip_q, theta_q))
def build_elliptic_rotor(blade_index: int, theta_q: int) -> np.ndarray:
if blade_index not in ELLIPTIC_PLANES:
raise ValueError(f"non-elliptic blade {blade_index}")
out = np.zeros(CL41_DIM, dtype=np.float64)
half = (theta_q * THETA_STEP) / 2.0
out[0] = math.cos(half)
out[blade_index] = math.sin(half)
return out
@dataclass(frozen=True, slots=True)
class VisionOperatorRegistry:
specs: dict[str, VisionOperatorSpec] = field(default_factory=dict)
basis_version: str = "vision-basis-v1"
def __getitem__(self, event_type: str) -> VisionOperatorSpec:
return self.specs[event_type]
def __contains__(self, event_type: str) -> bool:
return event_type in self.specs
def manifest_sha256(self) -> str:
payload = [
{
"operator_id": s.operator_id,
"event_type": s.event_type,
"blade_alias": s.blade_alias,
"blade_index": s.blade_index,
"base_theta_q": s.base_theta_q,
"gain_rules": [list(g) for g in s.gain_rules],
"theta_clip_q": s.theta_clip_q,
"version": s.version,
}
for s in sorted(self.specs.values(), key=lambda x: x.operator_id)
]
return sha256_json({"basis_version": self.basis_version, "operators": payload})
def _spec(op_id, etype, alias, blade, base, gains, clip=768) -> VisionOperatorSpec:
return VisionOperatorSpec(op_id, etype, alias, blade, base, tuple(gains), clip)
DEFAULT_OPERATOR_REGISTRY = VisionOperatorRegistry({
"region.flat": _spec("vision.region.flat.v1", "region.flat", "B_CONTRAST", 6, 24, [("contrast_q", 1)]),
"region.contrast": _spec("vision.region.contrast.v1", "region.contrast", "B_CONTRAST", 6, 64, [("contrast_q", 4)]),
"orient.edge_energy": _spec("vision.orient.edge_energy.v1", "orient.edge_energy", "B_ORIENT", 7, 48, [("orient_q", 3), ("energy_q", 4)]),
"texture.regularity": _spec("vision.texture.regularity.v1", "texture.regularity", "B_TEXTURE", 8, 40, [("texture_q", 5)]),
"salient.figure_ground": _spec("vision.salient.figure_ground.v1", "salient.figure_ground", "B_SALIENCE", 10, 56, [("salience_q", 4)]),
"region.chroma": _spec("vision.region.chroma.v1", "region.chroma", "B_CHROMA", 11, 36, [("chroma_q", 4)]),
"region.corner": _spec("vision.region.corner.v1", "region.corner", "B_CORNER", 13, 72, [("corner_q", 5)]),
"region.blob": _spec("vision.region.blob.v1", "region.blob", "B_BLOB", 10, 88, [("blob_q", 4)]),
"contour.closure": _spec("vision.contour.closure.v1", "contour.closure", "B_CONTOUR", 13, 104, [("closure_q", 3)]),
})